Showing preview only (318K chars total). Download the full file or copy to clipboard to get everything.
Repository: hackycy/sf-nest-admin
Branch: dev
Commit: d28a8b19abf5
Files: 139
Total size: 260.7 KB
Directory structure:
gitextract_j_pyb6i1/
├── .dockerignore
├── .eslintrc.js
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ ├── build-rc.yml
│ └── build-stable.yml
├── .gitignore
├── .prettierrc
├── Dockerfile
├── LICENSE
├── README.md
├── docs/
│ ├── sample/
│ │ ├── config.development.ts
│ │ └── docker-compose.yml
│ ├── 权限管理数据库设计文档.md
│ └── 通用协作规范.md
├── nest-cli.json
├── package.json
├── sql/
│ ├── init.sql
│ ├── upgrade_20210508.sql
│ ├── upgrade_20210914.sql.migrate
│ └── upgrade_20210927.sql
├── src/
│ ├── app.module.ts
│ ├── common/
│ │ ├── class/
│ │ │ └── res.class.ts
│ │ ├── contants/
│ │ │ ├── decorator.contants.ts
│ │ │ ├── error-code.contants.ts
│ │ │ └── param-config.contants.ts
│ │ ├── decorators/
│ │ │ └── keep.decorator.ts
│ │ ├── dto/
│ │ │ └── page.dto.ts
│ │ ├── exceptions/
│ │ │ ├── api.exception.ts
│ │ │ └── socket.exception.ts
│ │ ├── filters/
│ │ │ └── api-exception.filter.ts
│ │ └── interceptors/
│ │ └── api-transform.interceptor.ts
│ ├── config/
│ │ ├── config.default.ts
│ │ ├── config.production.ts
│ │ ├── configuration.ts
│ │ ├── defineConfig.ts
│ │ └── env.ts
│ ├── entities/
│ │ ├── admin/
│ │ │ ├── sys-config.entity.ts
│ │ │ ├── sys-department.entity.ts
│ │ │ ├── sys-login-log.entity.ts
│ │ │ ├── sys-menu.entity.ts
│ │ │ ├── sys-role-department.entity.ts
│ │ │ ├── sys-role-menu.entity.ts
│ │ │ ├── sys-role.entity.ts
│ │ │ ├── sys-task-log.entity.ts
│ │ │ ├── sys-task.entity.ts
│ │ │ ├── sys-user-role.entity.ts
│ │ │ └── sys-user.entity.ts
│ │ └── base.entity.ts
│ ├── main.ts
│ ├── mission/
│ │ ├── README.md
│ │ ├── jobs/
│ │ │ ├── http-request.job.ts
│ │ │ └── sys-log-clear.job.ts
│ │ ├── mission.decorator.ts
│ │ └── mission.module.ts
│ ├── modules/
│ │ ├── admin/
│ │ │ ├── account/
│ │ │ │ ├── account.controller.ts
│ │ │ │ ├── account.dto.ts
│ │ │ │ └── account.module.ts
│ │ │ ├── admin.constants.ts
│ │ │ ├── admin.interface.ts
│ │ │ ├── admin.module.ts
│ │ │ ├── core/
│ │ │ │ ├── decorators/
│ │ │ │ │ ├── admin-user.decorator.ts
│ │ │ │ │ ├── authorize.decorator.ts
│ │ │ │ │ ├── log-disabled.decorator.ts
│ │ │ │ │ └── permission-optional.decorator.ts
│ │ │ │ ├── guards/
│ │ │ │ │ └── auth.guard.ts
│ │ │ │ └── provider/
│ │ │ │ ├── qiniu.provider.ts
│ │ │ │ └── root-role-id.provider.ts
│ │ │ ├── login/
│ │ │ │ ├── login.class.ts
│ │ │ │ ├── login.controller.ts
│ │ │ │ ├── login.dto.ts
│ │ │ │ ├── login.module.ts
│ │ │ │ └── login.service.ts
│ │ │ ├── netdisk/
│ │ │ │ ├── manager/
│ │ │ │ │ ├── manage.class.ts
│ │ │ │ │ ├── manage.controller.ts
│ │ │ │ │ ├── manage.dto.ts
│ │ │ │ │ └── manage.service.ts
│ │ │ │ ├── netdisk.module.ts
│ │ │ │ └── overview/
│ │ │ │ ├── overview.class.ts
│ │ │ │ ├── overview.controller.ts
│ │ │ │ └── overview.service.ts
│ │ │ └── system/
│ │ │ ├── dept/
│ │ │ │ ├── dept.class.ts
│ │ │ │ ├── dept.controller.ts
│ │ │ │ ├── dept.dto.ts
│ │ │ │ └── dept.service.ts
│ │ │ ├── log/
│ │ │ │ ├── log.class.ts
│ │ │ │ ├── log.controller.ts
│ │ │ │ └── log.service.ts
│ │ │ ├── menu/
│ │ │ │ ├── menu.class.ts
│ │ │ │ ├── menu.controller.ts
│ │ │ │ ├── menu.dto.ts
│ │ │ │ └── menu.service.ts
│ │ │ ├── online/
│ │ │ │ ├── online.class.ts
│ │ │ │ ├── online.controller.ts
│ │ │ │ ├── online.dto.ts
│ │ │ │ └── online.service.ts
│ │ │ ├── param-config/
│ │ │ │ ├── param-config.controller.ts
│ │ │ │ ├── param-config.dto.ts
│ │ │ │ └── param-config.service.ts
│ │ │ ├── role/
│ │ │ │ ├── role.class.ts
│ │ │ │ ├── role.controller.ts
│ │ │ │ ├── role.dto.ts
│ │ │ │ └── role.service.ts
│ │ │ ├── serve/
│ │ │ │ ├── serve.class.ts
│ │ │ │ ├── serve.controller.ts
│ │ │ │ └── serve.service.ts
│ │ │ ├── system.module.ts
│ │ │ ├── task/
│ │ │ │ ├── task.controller.ts
│ │ │ │ ├── task.dto.ts
│ │ │ │ ├── task.processor.ts
│ │ │ │ └── task.service.ts
│ │ │ └── user/
│ │ │ ├── user.class.ts
│ │ │ ├── user.controller.ts
│ │ │ ├── user.dto.ts
│ │ │ └── user.service.ts
│ │ └── ws/
│ │ ├── admin-ws.gateway.ts
│ │ ├── admin-ws.guard.ts
│ │ ├── auth.service.ts
│ │ ├── ws.event.ts
│ │ └── ws.module.ts
│ ├── polyfill.ts
│ ├── setup-swagger.ts
│ └── shared/
│ ├── logger/
│ │ ├── logger.constants.ts
│ │ ├── logger.interface.ts
│ │ ├── logger.module.ts
│ │ ├── logger.service.ts
│ │ ├── typeorm-logger.service.ts
│ │ └── utils/
│ │ ├── app-root-path.util.ts
│ │ └── home-dir.ts
│ ├── redis/
│ │ ├── redis.constants.ts
│ │ ├── redis.interface.ts
│ │ └── redis.module.ts
│ ├── services/
│ │ ├── redis.service.ts
│ │ └── util.service.ts
│ └── shared.module.ts
├── test/
│ ├── app.e2e-spec.ts
│ └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
# compiled output
/dist
/node_modules
package-lock.json
yarn.lock
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Code
src/config/config.development.*
docs/*
sql/*
test/*
README.md
================================================
FILE: .eslintrc.js
================================================
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report(报告问题)
about: Create a report to help us improve
---
<!--
注意:为更好的解决你的问题,请参考模板提供完整信息,准确描述问题,信息不全的 issue 将被关闭。
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.
-->
## Bug report(问题描述)
#### Steps to reproduce(问题复现步骤)
<!--
1. [xxx]
2. [xxx]
3. [xxxx]
-->
#### Screenshot or Gif(截图或动态图)
#### Link to minimal reproduction(最小可在线还原demo)
<!--
Please only use Codepen, JSFiddle, CodeSandbox or a github repo
-->
#### Other relevant information(格外信息)
- Your OS:
- Node.js version:
- Mysql version:
- Redis version:
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature Request(新功能建议)
about: Suggest an idea for this project
---
## Feature request(新功能建议)
================================================
FILE: .github/workflows/build-rc.yml
================================================
name: Build RC Image
on:
create:
tags:
- '*'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/sfnestadmin:rc
-
name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
================================================
FILE: .github/workflows/build-stable.yml
================================================
name: Build Stable Image
on:
push:
branches:
- 'main'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/sfnestadmin:stable
-
name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
================================================
FILE: .gitignore
================================================
# compiled output
/dist
/node_modules
package-lock.json
yarn.lock
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Code
src/config/config.development.*
docs/sample/mysql/
================================================
FILE: .prettierrc
================================================
{
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all"
}
================================================
FILE: Dockerfile
================================================
FROM node:lts-alpine as builder
WORKDIR /sf-nest-admin
# set timezone
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' > /etc/timezone
# RUN npm set registry https://registry.npm.taobao.org
# cache step
COPY package.json /sf-nest-admin/package.json
RUN yarn install
# build
COPY ./ /sf-nest-admin
RUN yarn build
# clean dev dep
RUN rm -rf node_modules
RUN yarn install --production
# httpserver set port
EXPOSE 7001
# websokcet set port
EXPOSE 7002
CMD ["yarn", "start:prod"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021-present Changyuan Yang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# sf-nest-admin
   
**基于NestJs + TypeScript + TypeORM + Redis + MySql + Vue + Element-UI编写的一款简单高效的前后端分离的权限管理系统。希望这个项目在全栈的路上能够帮助到你。**
- 使用文档:[https://blog.si-yee.com/sf-admin-cli/](https://blog.si-yee.com/sf-admin-cli/)
- 演示站点:[http://opensource.admin.si-yee.com](http://opensource.admin.si-yee.com/)
- **Vue3版请移步:**[https://github.com/arklnk/ark-admin-nest](https://github.com/arklnk/ark-admin-nest)
- 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)
演示环境账号密码:
| 账号 | 密码 | 权限 |
| :----------: | :----: | :----------------------: |
| openadmin | 123456 | 仅只有各个功能的查询权限 |
| monitoradmin | 123456 | 系统监控页面及按钮权限 |
> 所有新建的用户初始密码都为123456
# 欢迎Star && PR
**如果项目有帮助到你可以点个Star支持下。有更好的实现欢迎PR。**
# LICENSE
[MIT](LICENSE)
================================================
FILE: docs/sample/config.development.ts
================================================
import * as qiniu from 'qiniu';
export default {
rootRoleId: 1,
// jwt sign secret
jwt: {
secret: process.env.JWT_SECRET || '123456',
},
// typeorm config
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'root',
password: '123456',
database: 'sf-admin',
synchronize: false,
logging: false,
},
redis: {
host: '127.0.0.1', // default value
port: 6379, // default value
password: '123456',
db: 0,
},
// qiniu config
qiniu: {
accessKey: 'xxx',
secretKey: 'xxx',
domain: 'xxx',
bucket: 'xxx',
zone: qiniu.zone.Zone_z0,
access: 'public',
},
};
================================================
FILE: docs/sample/docker-compose.yml
================================================
version: "2.0"
services:
db:
image: mysql:5.7.34
command: --default-authentication-plugin=mysql_native_password
restart: always
volumes:
- ./mysql:/var/lib/mysql/ # ./mysql路径可以替换成自己的路径
- ../../sql/:/docker-entrypoint-initdb.d/ # 初始化的脚本
ports:
- 3306:3306
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: sf-admin
MYSQL_USER: sf-admin
MYSQL_PASSWORD: 123456
redis:
image: redis:alpine
command: --requirepass "123456"
restart: always
ports:
- 6379:6379
environment:
TZ: Asia/Shanghai
sfserver:
image: qa894178522/sfnestadmin:stable
restart: always
depends_on:
- db
- redis
environment:
MYSQL_HOST: db
MYSQL_PORT: 3306
MYSQL_USERNAME: sf-admin
MYSQL_PASSWORD: 123456
MYSQL_DATABASE: sf-admin
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASSWORD: 123456
# 可选
MAILER_HOST: xxx
MAILER_PORT: xxx
MAILER_USER: xxx
MAILER_PASS: xxx
AMAP_KEY: xxx
QINIU_ACCESSKEY: xxx
QINIU_SECRETKEY: xxx
QINIU_DOMAIN: xxx
QINIU_BUCKET: xxx
QINIU_ZONE: xxx # Zone_as0 | Zone_na0 | Zone_z0 | Zone_z1 | Zone_z2
QINIU_ACCESS_TYPE: public # or private
sfvue:
image: qa894178522/sfvueadmin:nest
restart: always
environment:
TZ: Asia/Shanghai
depends_on:
- sfserver
ports:
- 7002:80
================================================
FILE: docs/权限管理数据库设计文档.md
================================================
# 文档修订记录
| 日期 | 版本 | 说明 | 作者 |
| :--------- | :----- | :--------------- | :------ |
| 2020-08-21 | v1.0.0 | 创建 | hackycy |
| 2021-09-27 | v2.0.0 | 增加sys_config表 | hackycy |
# 参考文献
https://blog.csdn.net/gglinux/article/details/68948901
https://www.zybuluo.com/stonezhou/note/1292262
http://www.csdeshang.com/home/dsmall/database.html
# 数据模型实体属性
## sys_menu
**表注释: 系统菜单**
| 字段 | 类型 | 空 | 默认 | 注释 |
| --------- | ------------ | ---- | ---- | ------------------------------- |
| id | int(20) | 否 | | ID |
| parent_id | int(20) | | | 父菜单ID |
| name | varchar(255) | 否 | | 菜单名称 |
| router | varchar(255) | | | 菜单地址 |
| perms | varchar(255) | | | 权限标识 |
| type | tinyint(4) | 否 | 0 | 类型,0:目录、1:菜单、2:按钮 |
| icon | varchar(255) | | | 对应图标 |
| order_num | int(11) | | 0 | 排序 |
| view_path | varchar(255) | | | 视图地址,对应vue文件 |
| keepalive | boolean | | true | 路由缓存 |
| Is_show | boolean | | true | 是否显示在菜单栏 |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ------ | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## sys_department
**表注释:系统部门**
| 字段 | 类型 | 空 | 默认 | 注释 |
| --------- | ------------ | ---- | ---- | ---------- |
| id | int(20) | 否 | | ID |
| parent_id | int(20) | | | 上级部门ID |
| name | varchar(255) | 否 | | 部门名称 |
| order_num | int(11) | | 0 | 排序 |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## sys_user
**表注释:系统用户**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------------- | ------------ | ---- | ---- | ------------------------------------------ |
| id | int(20) | 否 | | ID |
| department_id | int(20) | 否 | | 部门编号 |
| name | varchar(255) | 否 | | 姓名 |
| username | varchar(255) | 否 | | 登录账号 |
| password | varchar(255) | 否 | | 密码 |
| psalt | varchar(32) | 否 | | 密码盐值(随机生成,每个用户对应一个盐值) |
| nick_name | varchar(255) | | | 昵称 |
| head_img | varchar(255) | | | 头像 |
| email | varchar(255) | | | 邮箱 |
| phone | varchar(20) | | | 手机号 |
| remark | varchar(255) | | | 备注 |
| status | tinyint(4) | | 1 | 状态:0:禁用,1:启用 |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | -------- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
| UNIQUE | BTREE | 是 | 否 | username | | | 否 | |
## sys_role
**表注释:系统角色**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------- | ------------ | ---- | ---- | ------ |
| id | int(20) | 否 | | ID |
| user_id | varchar(255) | 否 | | 创建人 |
| name | varchar(255) | 否 | | 名称 |
| label | varchar(50) | 否 | | 标签 |
| remark | varchar(255) | | | 备注 |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ----- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
| UNIQUE | BTREE | 是 | 否 | label | | A | 否 | |
| UNIQUE | BTREE | 是 | 否 | name | | A | 否 | |
## sys_role_department
**表注释:系统角色部门关系**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------------- | ------- | ---- | ---- | ------ |
| id | int(20) | 否 | | ID |
| role_id | int(20) | 否 | | 角色ID |
| department_id | int(20) | 否 | | 部门ID |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## sys_role_menu
**表注释:系统角色菜单关系**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------- | ------- | ---- | ---- | ------ |
| id | int(20) | 否 | | ID |
| role_id | int(20) | 否 | | 角色ID |
| menu_id | int(20) | 否 | | 菜单ID |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## sys_user_role
**表注释:系统用户角色关系**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------- | ------- | ---- | ---- | ------ |
| id | int(20) | 否 | | ID |
| user_id | int(20) | 否 | | 用户ID |
| role_id | int(20) | 否 | | 角色ID |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## sys_login_log
**表注释:登录日志表**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------- | ------------ | ---- | ---- | ---------------------------- |
| id | int(20) | 否 | | ID |
| user_id | int(20) | | | 登录的用户id |
| ip | varchar(255) | | | 登录ip |
| time | datetime | | | 登陆时间(未使用,保留字段) |
| ua | varchar(500) | | | user-agent |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## sys_task
**表注释:系统任务表**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ---------- | ------------ | ---- | ---- | ------------------------------------- |
| id | int(20) | 否 | | ID |
| name | varchar(50) | 否 | | 任务名称 |
| service | varchar(255) | 否 | | 需要执行的service |
| type | tinyint(4) | 否 | 0 | 任务模式:0为cron,1为时间间隔 |
| status | tinyint(1) | | 0 | 任务状态:0为停止,1为运行 |
| start_time | datetime | | | 任务开始时间,type为0时生效 |
| end_time | datetime | | | 任务结束时间,type为0时生效 |
| limit | int | | 0 | 最大执行次数,小于或者等于0则不限次数 |
| cron | varchar(255) | | | cron表达式,type为0时生效 |
| every | int | | | 执行间隔,type为1时生效 |
| data | text | | | 传入数据 |
| job_opts | text | | | bull job options |
| remark | varchar(255) | | | 备注 |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
| UNIQUE | BTREE | 是 | 否 | name | | A | 否 | |
## sys_task_log
**表注释:任务日志表**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------- | ---------- | ---- | ---- | ------------------------------------- |
| id | int(20) | 否 | | id |
| task_id | int(20) | 否 | | 对应任务id |
| status | Tinyint(4) | 否 | 0 | 任务状态:0为失败,1为成功 |
| detail | Text | | | 任务详情 |
| consume_time | int(20) | | 0 | 任务完成耗时 (ms) |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
## sys_config
**表注释:系统参数配置表**
| 字段 | 类型 | 空 | 默认 | 注释 |
| ------ | ------------ | ---- | ---- | ------------ |
| id | int(20) | 否 | | id |
| key | varchar(50) | 否 | | 参数配置键 |
| value | varchar(255) | 是 | | 参数配置值 |
| name | varchar(50) | 否 | | 参数配置名称 |
| remark | varchar(255) | 是 | | 参数配置备注 |
**索引**
| 键名 | 类型 | 唯一 | 紧凑 | 字段 | 基数 | 排序规则 | 空 | 注释 |
| ------- | ----- | ---- | ---- | ---- | ---- | -------- | ---- | ---- |
| PRIMARY | BTREE | 是 | 否 | id | 0 | A | 否 | |
| UNIQUE | BTREE | 是 | 否 | key | | A | 否 | |
================================================
FILE: docs/通用协作规范.md
================================================
# 参考
https://github.com/GDJiaMi/frontend-standards
https://www.git-tower.com/learn/git/ebook/cn/command-line/advanced-topics/git-flow
# 1、工作流规范(基于Git)
## 1.1、开发
### 1.1.1、[版本规范](https://semver.org/lang/zh-CN/)
版本格式:主版本号.次版本号.修订号,版本号递增规则如下:
1. 主版本号:当你做了不兼容的 API 修改,
2. 次版本号:当你做了向下兼容的功能性新增,
3. 修订号:当你做了向下兼容的问题修正。
先行版本号及版本编译元数据可以加到“主版本号.次版本号.修订号”的后面,作为延伸。
### 1.1.2、Git分支模型
#### master分支
master分支表示一个稳定的发布版本. 对应百宝袋的大版本.
- 场景: 所有应用会跟随版本迭代, 在dev分支测试稳定后, 会合并到master分支, 并使用tag标记应用版本
- tag规范: `v{version}`, 例如v0.1.0
- 人员: 由项目负责人进行审核合并, 普通开发者没有权限
#### dev分支
开发者主要工作的分支, 最新的特性或bug修复都会提交到这个分支. 开发者如果在该分支进行了提交,在push到远程之前应该先pull一下, 并尽量使用rebase模式,保证分支的简洁
- 命名规范: dev
- tag规范: 在dev分支中也可能会经历发布过程, 例如bug修复版本. 这里同样使用tag来标记这些发布. 例如v0.1.1
- 提交规范:如果实在开发分支上进行开发,在推送到远程之前,应该使用`git rebase`形式更新本地分支。
#### feature分支
涉及多人协作或者大功能的开发, 应该从dev分支checkout出独立的feature分支, 避免干扰dev分支
- 场景:
- 涉及多人协作: 团队多个成员在同一个项目下负责开发不同的功能, 这时候每个成员在自己的feature分支独立开发
- 大功能开发: 大功能开发跨越周期比较长, 需要多次迭代才会稳定. 这时候应该在独立的分支上开发. 方便跟踪历史记录, 也免于干扰dev分支的迭代和发布
- 命名规范
- feature/name: name是功能名称
- feature/version: 这也是团队常见的模式, 当无法使用一个功能名称来描述时, 可以使用版本号作为’功能’
- 合并时机
1. 当feature分支迭代稳定, 并通过测试后, 合并到dev分支. 合并到dev后, **feature分支的生命周期就结束了**. 后续bug修复和功能优化直接在dev开发
2. 当多个feature分支需要合并对外发布临时版本时. 合并到preview分支 . ⚠️这种情况不应该合并到dev分支, 因为feature分支可能还不稳定或未完成. 比如为了联调某些功能.
- 合并方式
- 不要使用fast-forward. 这样可以在分支图上查看到分支历史
#### preview分支
临时的预览分支, preview分支用于临时合并feature分支, 这其中可能会修复某些bug或者冲突. 可以选择性地将这些提交cherrypick回feature分支. 当预览结束后就可以销毁preview分支
#### release分支
遵循gitflow规范
- 场景: 需要为某个正式版本修复bug(hotFix)时, 从master的对应tag中checkout release分支
- 命名规范: release/{version}
- 如何修复
+ 如果对应bug可以在dev分支直接被修复, 可以先提交到dev分支(或者已经修复了), 然后再cherrypick到release分支
+ 如果bug在新版本无法复现. 比如新版本升级了依赖. 那么在release分支直接修复即可
### 1.1.3、提交信息规范
#### 格式
我们采用angular的提交规范, 在这个规范的基础上支持(可选)`emoji`进行修饰
```
<type>(<scope>): <subject>
<body>
<footer>
```
##### header
> 如果提交时feature或者fix(已发布的版本), 这些提交信息应该出现在CHANGELOG
- type: 说明commit的类别. 可以配合emoji使用, 让阅读者更快地区分提交的类型,允许以下类型:
- feature或feat: 引入新功能
- fix: 修复了bug
- docs: 文档
- style: 优化项目结构或者代码格式
- refactor: 代码重构. 代码重构不涉及新功能和bug修复. 不应该影响原有功能, 包括对外暴露的接口
- test: 增加测试
- chore: 构建过程, 辅助工具升级. 如升级依赖, 升级构建工具
- perf: 性能优化
- revert: revert之前的commit
- git revert 命令用于撤销之前的一个提交, 并在为这个撤销操作生成一个提交
- build或release: 构建或发布版本
- ci: 持续集成
- types: 类型定义文件更改
- workflow: 工作流改进
- wip: 开发中
- safe: 修复安全问题
- scope: 可选. 说明提交影响的范围. 例如样式, 后端接口, 逻辑层等等
- Subject: 提交目的的简短描述, 动词开头, 不超过80个字符. 不要为了提交而提交
##### body
可选. 对本次提交的详细描述. 如果变动很简单, 可以省略
##### footer
可选. 只用于说明不兼容变动(break change)和关闭 Issue(如果使用使用gitlab或github管理bug的话)
#### 模板参考
https://github.com/angular/angular/commits/master
```
# 新增一条 Commit 记录
git commit -m 'chore(package.json): 新增 AngularJS 规范,Commit 时会自动调用钩子(GitHook)来判断 Message 是否有效'
# 搜索跟 package.json 文件相关的历史记录
git log HEAD --grep chore(package.json)
```
### 1.1.4、BUG处理规则
对于测试,目前会经历两个阶段
- 冒烟测试:在对测试正式发版之前会要求对代码进行自测,及冒烟测试。
- 正式测试阶段:正式测试阶段测试人员会在RDMS进行bug提交和管理,对BUG的处理规则如下:
- [解决待关闭]: 修改了程序代码, 问题解决;
- [不做处理]: 没有修改程序代码, 是由于其他原因(需求变更等), 而解决的问题;
- [退回]: 无规律或只出现一次的BUG, 研发没找到原因, 加上必要排查日志后, 可退回给测试; 复现后重新打开
- [正在处理]: 已大致定位原因, 需要较多时间处理的BUG, 可置为"正在处理"
> BUG的数量可能会和个人的KPI挂钩。所以要谨慎自测
### 1.1.5、处理定制化需求
- 痛点
- 对于定制化需求, 并不会引入到正规的代码流中, 一般情况下会checkout出一个分支, 来专门做这里定制化需求, 然后单独发版. 使用分支模式的缺点有:
- 更新问题
- 每次正规代码更新都要合并到该分支. 当分支较多时分支图就会比较混乱
- 正规代码合并是必然会带来风险的, 比如项目结构变动, 依赖库变动. 都可能导致定制化的代码失效
- 解决办法
- 减少代码耦合
- 尽量将定制化需求模块化, 最小化和正规代码之间的接触面. 这是解决该问题最根本的方式.
- 检验方式是结构变化时, 没有或很少适配代码
- 考虑通过代码层面区分
- 例如通过权限系统来配置. 通过后端接口动态配置
- 优先使用fork模式
- 有些场景确实无法通过代码层面解决, 比如ios应用定制启动图, icon, 应用名称, 外观等等. 这种方式优先使用fork模式, fork模式和分支模式没本质区别, 但是至少可以避免干扰正规开发流程
## 1.2、发布工作流
- 流程
1. 进行代码变更
2. 提交这些变更, 进行CI让这些变更通过测试
- 如果没通过就打tag, 一旦出现测试失败, tag就得重新打
3. 提升package.json的版本号, 更新CHANGELOG.md
4. 打上tag, 提交
5. 可选. 合并到release分支
- 工具
- 使用[jm-deploy release](https://github.com/carney520/jm-deploy)自动化发布并生成CHANGELOG.md
## 1.3、持续集成
所有项目基于coding的持续集成来完成。
## 1.4、扩展
- [如何写好 Git commit log?](https://www.zhihu.com/question/21209619)
- [提交信息emoji规范](https://gitmoji.carloscuesta.me/)
- [Commit message 和 Change log 编写指南](http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html)
- [Git远程操作详解](http://www.ruanyifeng.com/blog/2014/06/git_remote.html)
- [git钩子定制团队代码提交流程规范](https://www.jianshu.com/p/527e34f53b51)
- [保持fork之后的项目和上游同步](https://github.com/staticblog/wiki/wiki/保持fork之后的项目和上游同步)
# 2、编码规范
## 2.1、代码格式化
- [Prettier](https://prettier.io/) - 关于代码格式化的所有东西都交给它吧!
基本上,所有代码格式相关的工作都可以交给Prettier来做,在这个基础上再使用Eslint覆盖语义相关的检查
## 2.2、Code Review
#### Architecture/Design
- 单一职责原则.
- 这是经常被违背的原则。一个类只能干一个事情, 一个方法最好也只干一件事情。 比较常见的违背是一个类既干UI的事情,又干逻辑的事情, 这个在低质量的代码里很常见。
- 行为是否统一
- 比如缓存是否统一,错误处理是否统一, 错误提示是否统一, 弹出框是否统一 等等。
- 同一逻辑/同一行为 有没有走同一Code Path?低质量程序的另一个特征是,同一行为/同一逻辑,因为出现在不同的地方或者被不同的方式触发,没有走同一Code Path 或者各处有一份copy的实现, 导致非常难以维护。
- 代码污染
- 代码有没有对其他模块强耦合 ?
- 重复代码
- 主要看有没有把公用组件,可复用的代码,函数抽取出来。
- Open/Closed 原则
- 就是好不好扩展。 Open for extension, closed for modification.
- 面向接口编程 和 不是 面向实现编程
- 主要就是看有没有进行合适的抽象, 把一些行为抽象为接口。
- 健壮性
- 对Corner case有没有考虑完整,逻辑是否健壮?有没有潜在的bug?
- 有没有内存泄漏?有没有循环依赖?(针对特定语言,比如Objective-C) ?有没有野指针?
- 有没有考虑线程安全性, 数据访问的一致性
- 错误处理
- 有没有很好的Error Handling?比如网络出错,IO出错。
- 改动是不是对代码的提升
- 新的改动是打补丁,让代码质量继续恶化,还是对代码质量做了修复?
- 效率/性能
- 客户端程序 对频繁消息 和较大数据等耗时操作是否处理得当。
- 关键算法的时间复杂度多少?有没有可能有潜在的性能瓶颈。
其中有一部分问题,比如一些设计原则, 可预见的效率问题, 开发模式一致性的问题 应该尽早在Design Review阶段解决。如果Design阶段没有解决,那至少在Code Review阶段也要把它找出来。
#### Style
- 可读性
- 衡量可读性的可以有很好实践的标准,就是Reviewer能否非常容易的理解这个代码。 如果不是,那意味着代码的可读性要进行改进。
- 命名
- 命名对可读性非常重要,我倾向于函数名/方法名长一点都没关系,必须是能自我阐述的。
- 英语用词尽量准确一点(哪怕有时候需要借助Google Translate,是值得的)
- 函数长度/类长度
- 函数太长的不好阅读。 类太长了,比如超过了1000行,那你要看一下是否违反的“单一职责” 原则。
- 注释
- 恰到好处的注释。 但更多我看到比较差质量的工程的一个特点是缺少注释。
- 参数个数
- 不要太多, 一般不要超过3个。
#### Review Your Own Code First
- 跟著名的橡皮鸭调试法(Rubber Duck Debugging)一样,每次提交前整体把自己的代码过一遍非常有帮助,尤其是看看有没有犯低级错误。
#### 如何进行Code Review
- 多问问题。多问 “这块儿是怎么工作的?” “如果有XXX case,你这个怎么处理?”
- 每次提交的代码不要太多,最好不要超过1000行,否则review起来效率会非常低。
- 当面讨论代替Comments。 大部分情况下小组内的同事是坐在一起的,face to face的 code review是非常有效的。
- 区分重点,不要舍本逐末。 优先抓住 设计,可读性,健壮性等重点问题。
#### Code Review的意识
- 作为一个Developer , 不仅要Deliver working code, 还要Deliver maintainable code.
- 必要时进行重构,随着项目的迭代,在计划新增功能的同时,开发要主动计划重构的工作项。
- 开放的心态,虚心接受大家的Review Comments。
# 3、文档规范
## 3.1、文档中心
采用Coding提供的WIKI作为文档中心,采用Markdown格式。
可视化编辑器
- **Visual Code**: 大部分代码编辑都支持Markdown编辑和预览
- [**Mou**](https://link.jianshu.com/?t=http://mouapp.com/): Mac下的老牌编辑器
- [**typora**](https://typora.io/): 跨平台的Markdown编辑器,推荐
## 3.2、代码即文档
通过‘代码即文档’的方式至少可以**保持文档和代码同步更新**;另外**很多工具会分析代码的数据类型**,自动帮我们生成参数和返回值定义,这也可以减少很多文档编写工作以及出错率。
相关的工具有:
- API文档
- Typescript
- [tsdoc](https://github.com/microsoft/tsdoc) Typescript官方的注释文档标准
- [typedoc](https://github.com/TypeStrong/typedoc) 基于tsdoc标准的文档生成器
- Javascript
- [jsdoc](https://github.com/jsdoc/jsdoc) Javascript文档注释标准和生成器
- 后端接口文档
- [Swagger](https://swagger.io) Restful接口文档规范
- GraphQL: 这个有很多工具,例如[graphiql](https://github.com/graphql/graphiql), 集成了Playground和文档,很先进
- [Easy Mock](https://easy-mock.com/login) 一个可视化,并且能快速生成模拟数据的服务
- 组件文档
- [StoryBook](https://storybook.js.org) 通用的组件开发、测试、文档工具
- React
- [Docz](http://docz.site)
- [Styleguidist](https://github.com/styleguidist/react-styleguidist)
- Vue
- [vue-styleguidist](https://github.com/vue-styleguidist/vue-styleguidist)
## 3.3、注释即文档
**必要和适量的注释对阅读源代码的人来说就是一个路牌, 可以少走很多弯路**.
关于注释的一些准则,[<阿里巴巴Java开发手册>](https://github.com/alibaba/p3c/blob/master/p3c-gitbook/编程规约/注释规约.md)总结得非常好, 推荐基于这个来建立注释规范。另外通过ESlint是可以对注释进行一定程度的规范。
# 4、UI规范
待定
# 5、测试规范

## 单元测试
单元测试有很多**好处**, 比如:
- **提高信心,适应变化和迭代**. 如果现有代码有较为完善的单元测试,在代码重构时,可以检验模块是否依然可以工作, 一旦变更导致错误,单元测试也可以帮助我们快速定位并修复错误
- **单元测试是集成测试的基础**
- **测试即文档**。如果文档不能解决你的问题,在你打算看源码之前,可以查看单元测试。通过这些测试用例,开发人员可以直观地理解程序单元的基础API
- **提升代码质量。易于测试的代码,一般都是好代码**
**测什么?**
业务代码或业务组件是比较难以实施单元测试的,一方面它们比较多变、另一方面很多团队很少有精力维护这部分单元测试。所以**通常只要求对一些基础/底层的组件、框架或者服务进行测试, 视情况考虑是否要测试业务代码**
**测试的准则**:
- 推荐Petroware的[Unit Testing Guidelines](https://petroware.no/unittesting.html), 总结了27条单元测试准则,非常受用.
- 另外<阿里巴巴的Java开发手册>中总结的[单元测试准则](https://github.com/alibaba/p3c/blob/master/p3c-gitbook/单元测试.md), 也不错,虽然书名是Java,准则是通用的.
**单元测试指标**:
一般使用[`测试覆盖率`](https://zh.wikipedia.org/wiki/代碼覆蓋率)来量化,尽管对于覆盖率能不能衡量单元测试的有效性存在较多争议。
大部分情况下还是推荐尽可能提高覆盖率, 比如要求`语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%`. 视团队情况而定
扩展:
- [测试覆盖(率)到底有什么用?](https://www.infoq.cn/article/test-coverage-rate-role)
- [阿里巴巴Java开发文档-单元测试](https://www.kancloud.cn/kanglin/java_developers_guide/539190)
**相关工具**
- Headless Browsers: 无头浏览器是网页自动化的重要运行环境。 常用于功能测试、单元测试、网络爬虫
- [puppeteer](https://github.com/GoogleChrome/puppeteer)
- [Headless Chromium](https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md)
- 测试框架
- Jest
Facebook的单元测试框架. 零配置, 支持组件快照测试、模块Mock、Spy. 一般场景, 单元测试学它一个就行了
- 组件测试
- [testing-library](https://github.com/testing-library)
- [Enzyme](https://github.com/airbnb/enzyme)
- [Intern](https://theintern.github.io/)
- 单元测试
- [AVA](https://github.com/avajs/ava)
- [Jasmine](http://jasmine.github.io/)
- [Mocha](http://mochajs.org/)
- [Tape](https://github.com/substack/tape)
- 断言库
- [Chai](http://chaijs.com/)
- [expect.js](https://github.com/Automattic/expect.js)
- [should.js](http://shouldjs.github.io/)
- Mock/Stubs/Spies
- [sinon.js](http://sinonjs.org/)
- 代码覆盖率
- [istanbul](https://github.com/gotwarlost/istanbul)
- 基准测试
- [benchmark.js](http://benchmarkjs.com/)
- [jsperf.com](https://jsperf.com/)
# 6、异常处理、监控
## 6.1、异常处理
参考《阿里巴巴开发手册》中的[异常处理]([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))
## 6.2、日志
- 避免重复打印日志
- 谨慎地记录日志, 划分日志级别。比如生产环境禁止输出debug日志;有选择地输出info日志;
- 使用前缀对日志进行分类, 例如: `[User] xxxx`
## 6.2、异常监控
异常监控通常会通过三种方式来收集异常数据:
1. 全局捕获。
2. 主动上报。在try/catch中主动上报.
3. 用户反馈。比如弹窗让用户填写反馈信息.
第三方工具推荐
- [Bugly](https://bugly.qq.com/v2/) 免费
- [Sentry](https://sentry.io/welcome/) 免费基本够用
# 7、前后端协作规范
## 7.1、协作流程
前后端协作流程如下:

1、需求分析。参与者一般有前后端、测试、以及产品. 由产品主持,对需求进行宣贯,接受开发和测试的反馈,确保大家对需求有一致的认知
2、前后端开发讨论。讨论应用的一些开发设计,沟通技术点、难点、以及分工问题.
3、设计接口文档。可以由前后端一起设计;或者由后端设计、前端确认是否符合要求
4、并行开发。前后端并行开发,在这个阶段,前端可以先实现静态页面; 或者根据接口文档对接口进行Mock, 来模拟对接后端接口
5、在联调之前,要求后端做好接口测试
6、真实环境联调。前端将接口请求代理到后端服务,进行真实环境联调。
## 7.2、接口规范
采用RESTFUL设计规范。
**需要注意的点**:
- 明确区分是正常还是异常, 严格遵循接口的异常原语. 上述接口形式都有明确的异常原语,比如JSONRPC,当出现异常时应该返回`错误对象`响应,而不是在正常的响应体中返回错误代码. 另外要规范化的错误码, HTTP响应码就是一个不错的学习对象
- 明确数据类型。很多后端写的接口都是string和number不分的,如果妥协的话、前端就需要针对这个属性做特殊处理,这也可能是潜在的bug
- 明确空值的意义。比如在做更新操作是,空值是表示重置,还是忽略更新?
- 响应避免冗余的嵌套。
- 接口版本化,保持向下兼容。就像我们上文的‘语义化版本规范’说的,对于后端来说,API就是公共的接口. 公共暴露的接口应该有一个版本号,来说明当前描述的接口做了什么变动,是否向下兼容。 现在前端代码可能会在客户端被缓存,例如小程序。如果后端做了break change,就会影响这部分用户。
## 7.3、接口文档规范
后端通过接口文档向前端暴露接口相关的信息。通常需要包含这些信息:
- 版本号
- 文档描述
- 服务的入口. 例如基本路径
- 测试服务器. 可选
- 简单使用示例
- 安全和认证
- 请求限制
- 错误说明
- 版本
- 字段类型
- 具体接口定义
- 方法名称或者URL
- 方法描述
- 请求参数及其描述,必须说明类型(数据类型、是否可选等)
- 响应参数及其描述, 必须说明类型(数据类型、是否可选等)
- 可能的异常情况、错误代码、以及描述
- 请求示例,可选
> 也可采用Coding提供的API文档模板来改写
**人工维护导致的问题**:
上文‘代码即文档’就提到了人工维护接口文档可能导致代码和文档不同步问题。
如果可以从代码或者规范文档(例如OpenAPI这类API描述规范)中生成接口文档,可以解决实现和文档不一致问题, 同时也可以减少文档编写和维护的投入.
**项目采用Coding提供的API文档来自动生成API文档。**
================================================
FILE: nest-cli.json
================================================
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}
================================================
FILE: package.json
================================================
{
"name": "sf-nest-admin",
"version": "2.3.3",
"description": "simple and efficient authority management system with separation of front and backends",
"author": "hackycy",
"private": true,
"license": "MIT",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "cross-env NODE_ENV=development nest start",
"dev": "cross-env NODE_ENV=development nest start --watch",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main.js",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"cleanlog": "rimraf logs"
},
"dependencies": {
"@nestjs/axios": "^0.0.2",
"@nestjs/bull": "^0.4.1",
"@nestjs/common": "^8.4.6",
"@nestjs/config": "^1.1.5",
"@nestjs/core": "^8.4.6",
"@nestjs/jwt": "^8.0.0",
"@nestjs/platform-fastify": "^8.4.6",
"@nestjs/platform-socket.io": "^8.4.6",
"@nestjs/swagger": "^5.1.3",
"@nestjs/typeorm": "^8.1.2",
"@nestjs/websockets": "^8.4.6",
"bull": "^3.22.5",
"cache-manager": "^3.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"cross-env": "^7.0.3",
"crypto-js": "^4.0.0",
"date-fns": "^2.21.3",
"fastify-swagger": "^5.2.0",
"ioredis": "^4.27.6",
"lodash": "^4.17.21",
"mysql2": "^2.2.5",
"nanoid": "^3.1.22",
"qiniu": "^7.3.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"svg-captcha": "^1.4.0",
"systeminformation": "^5.9.4",
"typeorm": "^0.3.6",
"ua-parser-js": "^0.7.28",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.5"
},
"devDependencies": {
"@nestjs/cli": "^8.1.1",
"@nestjs/schematics": "^8.0.2",
"@nestjs/testing": "^8.4.6",
"@types/bull": "^3.15.1",
"@types/cache-manager": "^3.4.0",
"@types/jest": "^28.1.0",
"@types/lodash": "^4.14.168",
"@types/node": "^14.14.36",
"@types/supertest": "^2.0.10",
"@types/ua-parser-js": "^0.7.35",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"jest": "^28.1.0",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"supertest": "^6.1.3",
"ts-jest": "^28.0.4",
"ts-loader": "^9.3.0",
"ts-node": "^10.8.0",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.2.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
================================================
FILE: sql/init.sql
================================================
/*
Date: 26/10/2020 17:30:38
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_department
-- ----------------------------
DROP TABLE IF EXISTS `sys_department`;
CREATE TABLE `sys_department` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) DEFAULT NULL,
`name` varchar(255) NOT NULL,
`order_num` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_department
-- ----------------------------
BEGIN;
INSERT INTO `sys_department` VALUES ('2020-08-27 03:33:19.000000', '2020-08-27 03:33:19.000000', 1, NULL, '思忆技术', 0);
INSERT INTO `sys_department` VALUES ('2020-09-08 05:31:32.426851', '2020-10-07 04:25:31.000000', 2, 1, '管理部门', 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_login_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_login_log`;
CREATE TABLE `sys_login_log` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(20) DEFAULT NULL,
`ip` varchar(255) DEFAULT NULL,
`time` datetime DEFAULT NULL,
`ua` varchar(500) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`parent_id` int(11) DEFAULT NULL,
`name` varchar(255) NOT NULL,
`router` varchar(255) DEFAULT NULL,
`perms` varchar(255) DEFAULT NULL,
`type` tinyint(4) NOT NULL DEFAULT '0',
`icon` varchar(255) DEFAULT NULL,
`order_num` int(11) DEFAULT '0',
`view_path` varchar(255) DEFAULT NULL,
`keepalive` tinyint(4) DEFAULT '1',
`is_show` tinyint(4) DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=69 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
BEGIN;
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
COMMIT;
-- ----------------------------
-- Table structure for sys_req_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_req_log`;
CREATE TABLE `sys_req_log` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`ip` varchar(255) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`params` text,
`action` varchar(100) DEFAULT NULL,
`method` varchar(15) DEFAULT NULL,
`status` int(11) DEFAULT NULL,
`consume_time` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) NOT NULL,
`name` varchar(255) NOT NULL,
`label` varchar(50) NOT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_223de54d6badbe43a5490450c3` (`name`),
UNIQUE KEY `IDX_f2d07943355da93c3a8a1c411a` (`label`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` VALUES ('2020-08-27 03:35:05.000000', '2020-08-27 03:35:05.000000', 1, 'root', 'root', '超级管理员', NULL);
COMMIT;
-- ----------------------------
-- Table structure for sys_role_department
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_department`;
CREATE TABLE `sys_role_department` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) NOT NULL,
`department_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) NOT NULL,
`menu_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_task
-- ----------------------------
DROP TABLE IF EXISTS `sys_task`;
CREATE TABLE `sys_task` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`service` varchar(255) NOT NULL,
`type` tinyint(4) NOT NULL DEFAULT '0',
`status` tinyint(4) NOT NULL DEFAULT '1',
`start_time` datetime DEFAULT NULL,
`end_time` datetime DEFAULT NULL,
`limit` int(11) DEFAULT '0',
`cron` varchar(255) DEFAULT NULL,
`every` int(11) DEFAULT NULL,
`data` text,
`job_opts` text,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_ef8e5ab5ef2fe0ddb1428439ef` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_task
-- ----------------------------
BEGIN;
-- 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}', '');
INSERT 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}', '');
INSERT 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}', '');
COMMIT;
-- ----------------------------
-- Table structure for sys_task_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_task_log`;
CREATE TABLE `sys_task_log` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`task_id` int(11) NOT NULL,
`status` tinyint(4) NOT NULL DEFAULT '0',
`detail` text,
`consume_time` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`department_id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`nick_name` varchar(255) DEFAULT NULL,
`head_img` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`phone` varchar(255) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL,
`psalt` varchar(32) NOT NULL,
`status` tinyint(4) DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_9e7164b2f1ea1348bc0eb0a7da` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT 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);
COMMIT;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_user_role` VALUES ('2020-09-14 04:10:34.371646', '2020-09-14 04:10:34.371646', 1, 1, 1);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
================================================
FILE: sql/upgrade_20210508.sql
================================================
-- ----------------------------
-- 增加七牛文件空间
-- ----------------------------
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
--
-- 转存表中的数据 `sys_menu`
--
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
INSERT 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);
================================================
FILE: sql/upgrade_20210914.sql.migrate
================================================
-- 用于旧版本兼容迁移脚本,请手动执行
DROP TABLE IF EXISTS `sys_req_log`;
ALTER TABLE `sys_department` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_department` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_department` CHANGE parend_id parent_id int(11) DEFAULT NULL;
ALTER TABLE `sys_login_log` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_login_log` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_menu` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_menu` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_menu` CHANGE isShow is_show tinyint(4) DEFAULT '1';
ALTER TABLE `sys_role` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_role` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_role` CHANGE userId user_id varchar(255) NOT NULL;
ALTER TABLE `sys_role_department` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_role_department` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_role_menu` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_role_menu` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_task` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_task` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_task_log` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_task_log` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_user` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_user` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_user_role` CHANGE createTime created_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
ALTER TABLE `sys_user_role` CHANGE updateTime updated_at datetime(6) DEFAULT CURRENT_TIMESTAMP(6) NOT NULL;
DELETE FROM `sys_menu` WHERE id = 38;
DELETE FROM `sys_role_menu` WHERE `menu_id` = 38;
DELETE FROM `sys_task` WHERE id = 1;
================================================
FILE: sql/upgrade_20210927.sql
================================================
-- `sf-admin`.sys_config definition
DROP TABLE IF EXISTS `sys_config`;
CREATE TABLE `sys_config` (
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`id` int(11) NOT NULL AUTO_INCREMENT,
`key` varchar(50) NOT NULL,
`name` varchar(50) NOT NULL,
`value` varchar(255) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_2c363c25cf99bcaab3a7f389ba` (`key`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
BEGIN;
--
-- 转存表中的数据 `sys_menu`
--
INSERT INTO `sys_menu` (`created_at`, `updated_at`, `id`, `parent_id`, `name`, `router`, `perms`, `type`, `icon`, `order_num`, `view_path`, `keepalive`, `is_show`) VALUES
('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),
('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),
('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),
('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),
('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),
('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),
('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);
--
-- 转存表中的数据 `sys_config`
--
INSERT INTO `sys_config` (created_at,updated_at,`key`,name,value,remark) VALUES
('2021-09-28 03:14:05.256120000','2021-09-28 03:14:05.256120000','sys_user_initPassword','初始密码','123456','创建管理员账号的初始密码');
COMMIT;
================================================
FILE: src/app.module.ts
================================================
import './polyfill';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BullModule } from '@nestjs/bull';
import Configuration from './config/configuration';
import { AdminModule } from './modules/admin/admin.module';
import { SharedModule } from './shared/shared.module';
import { MissionModule } from './mission/mission.module';
import { WSModule } from './modules/ws/ws.module';
import { LoggerModule } from './shared/logger/logger.module';
import {
LoggerModuleOptions,
WinstonLogLevel,
} from './shared/logger/logger.interface';
import { TypeORMLoggerService } from './shared/logger/typeorm-logger.service';
import { LOGGER_MODULE_OPTIONS } from './shared/logger/logger.constants';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [Configuration],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule, LoggerModule],
useFactory: (
configService: ConfigService,
loggerOptions: LoggerModuleOptions,
) => ({
autoLoadEntities: true,
type: configService.get<any>('database.type'),
host: configService.get<string>('database.host'),
port: configService.get<number>('database.port'),
username: configService.get<string>('database.username'),
password: configService.get<string>('database.password'),
database: configService.get<string>('database.database'),
synchronize: configService.get<boolean>('database.synchronize'),
logging: configService.get('database.logging'),
// 自定义日志
logger: new TypeORMLoggerService(
configService.get('database.logging'),
loggerOptions,
),
}),
inject: [ConfigService, LOGGER_MODULE_OPTIONS],
}),
BullModule.forRoot({}),
// custom logger
LoggerModule.forRootAsync(
{
imports: [ConfigModule],
useFactory: (configService: ConfigService) => {
return {
level: configService.get<WinstonLogLevel>('logger.level'),
consoleLevel: configService.get<WinstonLogLevel>(
'logger.consoleLevel',
),
timestamp: configService.get<boolean>('logger.timestamp'),
maxFiles: configService.get<string>('logger.maxFiles'),
maxFileSize: configService.get<string>('logger.maxFileSize'),
disableConsoleAtProd: configService.get<boolean>(
'logger.disableConsoleAtProd',
),
dir: configService.get<string>('logger.dir'),
errorLogName: configService.get<string>('logger.errorLogName'),
appLogName: configService.get<string>('logger.appLogName'),
};
},
inject: [ConfigService],
},
// global module
true,
),
// custom module
SharedModule,
// mission module
MissionModule.forRoot(),
// application modules import
AdminModule,
// websocket module
WSModule,
],
})
export class AppModule {}
================================================
FILE: src/common/class/res.class.ts
================================================
export class ResOp {
readonly data: any;
readonly code: number;
readonly message: string;
constructor(code: number, data?: any, message = 'success') {
this.code = code;
this.data = data;
this.message = message;
}
static success(data?: any) {
return new ResOp(200, data);
}
}
export class Pagination {
total: number;
page: number;
size: number;
}
export class PageResult<T> {
list?: Array<T>;
pagination: Pagination;
}
================================================
FILE: src/common/contants/decorator.contants.ts
================================================
// @Keep
export const TRANSFORM_KEEP_KEY_METADATA = 'common:transform_keep';
// @Mission
export const MISSION_KEY_METADATA = 'common:mission';
================================================
FILE: src/common/contants/error-code.contants.ts
================================================
/**
* 统一错误代码定义
*/
export const ErrorCodeMap = {
// 10000 - 99999 业务操作错误
10000: '参数校验异常',
10001: '系统用户已存在',
10002: '填写验证码有误',
10003: '用户名密码有误',
10004: '节点路由已存在',
10005: '权限必须包含父节点',
10006: '非法操作:该节点仅支持目录类型父节点',
10007: '非法操作:节点类型无法直接转换',
10008: '该角色存在关联用户,请先删除关联用户',
10009: '该部门存在关联用户,请先删除关联用户',
10010: '该部门存在关联角色,请先删除关联角色',
10015: '该部门存在子部门,请先删除子部门',
10011: '旧密码与原密码不一致',
10012: '如想下线自身可右上角退出',
10013: '不允许下线该用户',
10014: '父级菜单不存在',
10016: '系统内置功能不允许操作',
10017: '用户不存在',
10018: '无法查找当前用户所属部门',
10019: '部门不存在',
10020: '任务不存在',
10021: '参数配置键值对已存在',
10101: '不安全的任务,确保执行的加入@Mission注解',
10102: '所执行的任务不存在',
// token相关
11001: '登录无效或无权限访问',
11002: '登录身份已过期',
11003: '无权限,请联系管理员申请权限',
// OSS相关
20001: '当前创建的文件或目录已存在',
20002: '无需操作',
20003: '已超出支持的最大处理数量',
};
================================================
FILE: src/common/contants/param-config.contants.ts
================================================
export const SYS_USER_INITPASSWORD = 'sys_user_initPassword';
================================================
FILE: src/common/decorators/keep.decorator.ts
================================================
import { SetMetadata } from '@nestjs/common';
import { TRANSFORM_KEEP_KEY_METADATA } from '../contants/decorator.contants';
/**
* 不转化成JSON结构,保留原有返回
*/
export const Keep = () => SetMetadata(TRANSFORM_KEEP_KEY_METADATA, true);
================================================
FILE: src/common/dto/page.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, Min } from 'class-validator';
export class PageOptionsDto {
@ApiProperty({
description: '当前页包含数量',
required: false,
default: 10,
})
@Type(() => Number)
@IsInt()
@Min(1)
readonly limit: number = 10;
@ApiProperty({
description: '当前页包含数量',
required: false,
default: 1,
})
@Type(() => Number)
@IsInt()
@Min(1)
readonly page: number = 1;
}
================================================
FILE: src/common/exceptions/api.exception.ts
================================================
import { HttpException } from '@nestjs/common';
import { ErrorCodeMap } from '../contants/error-code.contants';
/**
* Api业务异常均抛出该异常
*/
export class ApiException extends HttpException {
/**
* 业务类型错误代码,非Http code
*/
private errorCode: number;
constructor(errorCode: number) {
super(ErrorCodeMap[errorCode], 200);
this.errorCode = errorCode;
}
getErrorCode(): number {
return this.errorCode;
}
}
================================================
FILE: src/common/exceptions/socket.exception.ts
================================================
import { WsException } from '@nestjs/websockets';
import { ErrorCodeMap } from '../contants/error-code.contants';
export class SocketException extends WsException {
private errorCode: number;
constructor(errorCode: number) {
super(ErrorCodeMap[errorCode]);
this.errorCode = errorCode;
}
getErrorCode(): number {
return this.errorCode;
}
}
================================================
FILE: src/common/filters/api-exception.filter.ts
================================================
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { FastifyReply } from 'fastify';
import { isDev } from 'src/config/env';
import { ApiException } from '../exceptions/api.exception';
import { ResOp } from '../class/res.class';
import { LoggerService } from 'src/shared/logger/logger.service';
/**
* 异常接管,统一异常返回数据
*/
@Catch()
export class ApiExceptionFilter implements ExceptionFilter {
constructor(private logger: LoggerService) {}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<FastifyReply>();
// check api exection
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
// set json response
response.header('Content-Type', 'application/json; charset=utf-8');
// prod env will not return internal error message
const code =
exception instanceof ApiException
? (exception as ApiException).getErrorCode()
: status;
let message = '服务器异常,请稍后再试';
// 开发模式下提示500类型错误,生产模式下屏蔽500内部错误提示
if (isDev() || status < 500) {
message =
exception instanceof HttpException ? exception.message : `${exception}`;
}
// 记录 500 日志
if (status >= 500) {
this.logger.error(exception, ApiExceptionFilter.name);
}
const result = new ResOp(code, null, message);
response.status(status).send(result);
}
}
================================================
FILE: src/common/interceptors/api-transform.interceptor.ts
================================================
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { FastifyReply } from 'fastify';
import { map } from 'rxjs/operators';
import { TRANSFORM_KEEP_KEY_METADATA } from '../contants/decorator.contants';
import { ResOp } from '../class/res.class';
/**
* 统一处理返回接口结果,如果不需要则添加@Keep装饰器
*/
export class ApiTransformInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
return next.handle().pipe(
map((data) => {
const keep = this.reflector.get<boolean>(
TRANSFORM_KEEP_KEY_METADATA,
context.getHandler(),
);
if (keep) {
return data;
} else {
const response = context.switchToHttp().getResponse<FastifyReply>();
response.header('Content-Type', 'application/json; charset=utf-8');
return new ResOp(200, data);
}
}),
);
}
}
================================================
FILE: src/config/config.default.ts
================================================
import { defineConfig } from './defineConfig';
/**
* 项目通用默认配置,但优先级最低
*/
export default defineConfig({
rootRoleId: parseInt(process.env.ROOT_ROLE_ID || '1'),
});
================================================
FILE: src/config/config.production.ts
================================================
import * as qiniu from 'qiniu';
import { defineConfig } from './defineConfig';
const parseZone = (zone: string) => {
switch (zone) {
case 'Zone_as0':
return qiniu.zone.Zone_as0;
case 'Zone_na0':
return qiniu.zone.Zone_na0;
case 'Zone_z0':
return qiniu.zone.Zone_z0;
case 'Zone_z1':
return qiniu.zone.Zone_z1;
case 'Zone_z2':
return qiniu.zone.Zone_z2;
}
};
export default defineConfig({
jwt: {
secret: process.env.JWT_SECRET || '123456',
},
// typeorm config
database: {
type: 'mysql',
host: process.env.MYSQL_HOST || '127.0.0.1',
port: process.env.MYSQL_PORT || 3306,
username: process.env.MYSQL_USERNAME || 'root',
password: process.env.MYSQL_PASSWORD || '123456',
database: process.env.MYSQL_DATABASE || 'sf-admin',
synchronize: false,
logging: ['error'],
},
// redis cache config
redis: {
host: process.env.REDIS_HOST || '127.0.0.1', // default value
port: parseInt(process.env.REDIS_PORT) || 6379, // default value
password: process.env.REDIS_PASSWORD || '123456',
db: 0,
},
// qiniu config
qiniu: {
accessKey: process.env.QINIU_ACCESSKEY,
secretKey: process.env.QINIU_SECRETKEY,
domain: process.env.QINIU_DOMAIN,
bucket: process.env.QINIU_BUCKET,
zone: parseZone(process.env.QINIU_ZONE || 'Zone_z2'),
access: (process.env.QINIU_ACCESS_TYPE as any) || 'public',
},
// logger config
logger: {
timestamp: false,
dir: process.env.LOGGER_DIR,
maxFileSize: process.env.LOGGER_MAX_SIZE,
maxFiles: process.env.LOGGER_MAX_FILES,
errorLogName: process.env.LOGGER_ERROR_FILENAME,
appLogName: process.env.LOGGER_APP_FILENAME,
},
// swagger
swagger: {
enable: process.env.SWAGGER_ENABLE === 'true',
path: process.env.SWAGGER_PATH,
title: process.env.SWAGGER_TITLE,
desc: process.env.SWAGGER_DESC,
version: process.env.SWAGGER_VERSION,
},
});
================================================
FILE: src/config/configuration.ts
================================================
import { merge } from 'lodash';
import DefaultConfig from './config.default';
import { IConfig } from './defineConfig';
/**
* 根据环境变量判断使用配置
*/
export default () => {
let envConfig: IConfig = {};
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
envConfig = require(`./config.${process.env.NODE_ENV}`).default;
} catch (e) {
// 无效配置则自动忽略
}
// 合并配置
return merge(DefaultConfig, envConfig);
};
================================================
FILE: src/config/defineConfig.ts
================================================
import { conf } from 'qiniu';
import { LoggerModuleOptions as LoggerConfigOptions } from 'src/shared/logger/logger.interface';
import { LoggerOptions } from 'typeorm';
/**
* 用于智能提示
*/
export function defineConfig(config: IConfig): IConfig {
return config;
}
/**
* sf-admin 配置
*/
export interface IConfig {
/**
* 管理员角色ID,一旦分配,该角色下分配的管理员都为超级管理员
*/
rootRoleId?: number;
/**
* 用户鉴权Token密钥
*/
jwt?: JwtConfigOptions;
/**
* Mysql数据库配置
*/
database?: DataBaseConfigOptions;
/**
* Redis配置
*/
redis?: RedisConfigOptions;
/**
* 七牛云配置
*/
qiniu?: QiniuConfigOptions;
/**
* 应用级别日志配置
*/
logger?: LoggerConfigOptions;
/**
* Swagger文档配置
*/
swagger?: SwaggerConfigOptions;
}
//--------- config interface ------------
export interface JwtConfigOptions {
secret: string;
}
export interface QiniuConfigOptions {
accessKey?: string;
secretKey?: string;
bucket?: string;
zone?: conf.Zone;
domain?: string;
access?: string;
}
export interface RedisConfigOptions {
host?: string;
port?: number | string;
password?: string;
db?: number;
}
export interface DataBaseConfigOptions {
type?: string;
host?: string;
port?: number | string;
username?: string;
password?: string;
database?: string;
synchronize?: boolean;
logging?: LoggerOptions;
}
export interface SwaggerConfigOptions {
enable?: boolean;
path?: string;
title?: string;
desc?: string;
version?: string;
}
================================================
FILE: src/config/env.ts
================================================
/**
* check dev env
* @returns boolean true is dev
*/
export function isDev(): boolean {
return process.env.NODE_ENV === 'development';
}
================================================
FILE: src/entities/admin/sys-config.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_config' })
export default class SysConfig extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ type: 'varchar', length: 50, unique: true })
@ApiProperty()
key: string;
@Column({ type: 'varchar', length: 50 })
@ApiProperty()
name: string;
@Column({ type: 'varchar', nullable: true })
@ApiProperty()
value: string;
@Column({ type: 'varchar', nullable: true })
@ApiProperty()
remark: string;
}
================================================
FILE: src/entities/admin/sys-department.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_department' })
export default class SysDepartment extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'parent_id', nullable: true })
@ApiProperty()
parentId: number;
@Column()
@ApiProperty()
name: string;
@Column({ name: 'order_num', type: 'int', nullable: true, default: 0 })
@ApiProperty()
orderNum: number;
}
================================================
FILE: src/entities/admin/sys-login-log.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_login_log' })
export default class SysLoginLog extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ nullable: true, name: 'user_id' })
@ApiProperty()
userId: number;
@Column({ nullable: true })
@ApiProperty()
ip: string;
@Column({ type: 'datetime', nullable: true })
@ApiProperty()
time: Date;
@Column({ length: 500, nullable: true })
@ApiProperty()
ua: string;
}
================================================
FILE: src/entities/admin/sys-menu.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_menu' })
export default class SysMenu extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'parent_id', nullable: true })
@ApiProperty()
parentId: number;
@Column()
@ApiProperty()
name: string;
@Column({ nullable: true })
@ApiProperty()
router: string;
@Column({ nullable: true })
@ApiProperty()
perms: string;
@Column({ type: 'tinyint', default: 0 })
@ApiProperty()
type: number;
@Column({ nullable: true })
@ApiProperty()
icon: string;
@Column({ name: 'order_num', type: 'int', default: 0, nullable: true })
@ApiProperty()
orderNum: number;
@Column({ name: 'view_path', nullable: true })
@ApiProperty()
viewPath: string;
@Column({ type: 'boolean', nullable: true, default: true })
@ApiProperty()
keepalive: boolean;
@Column({ name: 'is_show', type: 'boolean', nullable: true, default: true })
@ApiProperty()
isShow: boolean;
}
================================================
FILE: src/entities/admin/sys-role-department.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_role_department' })
export default class SysRoleDepartment extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'role_id' })
@ApiProperty()
roleId: number;
@Column({ name: 'department_id' })
@ApiProperty()
departmentId: number;
}
================================================
FILE: src/entities/admin/sys-role-menu.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_role_menu' })
export default class SysRoleMenu extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'role_id' })
@ApiProperty()
roleId: number;
@Column({ name: 'menu_id' })
@ApiProperty()
menuId: number;
}
================================================
FILE: src/entities/admin/sys-role.entity.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_role' })
export default class SysRole extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'user_id' })
@ApiProperty()
userId: string;
@Column({ unique: true })
@ApiProperty()
name: string;
@Column({ length: 50, unique: true })
@ApiProperty()
label: string;
@Column({ nullable: true })
@ApiProperty()
remark: string;
}
================================================
FILE: src/entities/admin/sys-task-log.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_task_log' })
export default class SysTaskLog extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'task_id' })
@ApiProperty()
taskId: number;
@Column({ type: 'tinyint', default: 0 })
@ApiProperty()
status: number;
@Column({ type: 'text', nullable: true })
@ApiProperty()
detail: string;
@Column({ type: 'int', nullable: true, name: 'consume_time', default: 0 })
@ApiProperty()
consumeTime: number;
}
================================================
FILE: src/entities/admin/sys-task.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_task' })
export default class SysTask extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ type: 'varchar', length: 50, unique: true })
@ApiProperty()
name: string;
@Column()
@ApiProperty()
service: string;
@Column({ type: 'tinyint', default: 0 })
@ApiProperty()
type: number;
@Column({ type: 'tinyint', default: 1 })
@ApiProperty()
status: number;
@Column({ name: 'start_time', type: 'datetime', nullable: true })
@ApiProperty()
startTime: Date;
@Column({ name: 'end_time', type: 'datetime', nullable: true })
@ApiProperty()
endTime: Date;
@Column({ type: 'int', nullable: true, default: 0 })
@ApiProperty()
limit: number;
@Column({ nullable: true })
@ApiProperty()
cron: string;
@Column({ type: 'int', nullable: true })
@ApiProperty()
every: number;
@Column({ type: 'text', nullable: true })
@ApiProperty()
data: string;
@Column({ name: 'job_opts', type: 'text', nullable: true })
@ApiProperty()
jobOpts: string;
@Column({ nullable: true })
@ApiProperty()
remark: string;
}
================================================
FILE: src/entities/admin/sys-user-role.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_user_role' })
export default class SysUserRole extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'user_id' })
@ApiProperty()
userId: number;
@Column({ name: 'role_id' })
@ApiProperty()
roleId: number;
}
================================================
FILE: src/entities/admin/sys-user.entity.ts
================================================
import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from '../base.entity';
@Entity({ name: 'sys_user' })
export default class SysUser extends BaseEntity {
@PrimaryGeneratedColumn()
@ApiProperty()
id: number;
@Column({ name: 'department_id' })
@ApiProperty()
departmentId: number;
@Column()
@ApiProperty()
name: string;
@Column({ unique: true })
@ApiProperty()
username: string;
@Column()
@ApiProperty()
password: string;
@Column({ length: 32 })
@ApiProperty()
psalt: string;
@Column({ name: 'nick_name', nullable: true })
@ApiProperty()
nickName: string;
@Column({ name: 'head_img', nullable: true })
@ApiProperty()
headImg: string;
@Column({ nullable: true })
@ApiProperty()
email: string;
@Column({ nullable: true })
@ApiProperty()
phone: string;
@Column({ nullable: true })
@ApiProperty()
remark: string;
@Column({ type: 'tinyint', nullable: true, default: 1 })
@ApiProperty()
status: number;
}
================================================
FILE: src/entities/base.entity.ts
================================================
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
export abstract class BaseEntity {
@CreateDateColumn({ name: 'created_at' })
@ApiProperty()
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
@ApiProperty()
updatedAt: Date;
}
================================================
FILE: src/main.ts
================================================
import {
HttpStatus,
UnprocessableEntityException,
ValidationPipe,
} from '@nestjs/common';
import { NestFactory, Reflector } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ValidationError } from 'class-validator';
import { flatten } from 'lodash';
import { AppModule } from './app.module';
import { ApiExceptionFilter } from './common/filters/api-exception.filter';
import { ApiTransformInterceptor } from './common/interceptors/api-transform.interceptor';
import { setupSwagger } from './setup-swagger';
import { LoggerService } from './shared/logger/logger.service';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
{
bufferLogs: true,
},
);
// custom logger
app.useLogger(app.get(LoggerService));
// validate
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
exceptionFactory: (errors: ValidationError[]) => {
return new UnprocessableEntityException(
flatten(
errors
.filter((item) => !!item.constraints)
.map((item) => Object.values(item.constraints)),
).join('; '),
);
},
}),
);
// execption
app.useGlobalFilters(new ApiExceptionFilter(app.get(LoggerService)));
// api interceptor
app.useGlobalInterceptors(new ApiTransformInterceptor(new Reflector()));
// websocket
app.useWebSocketAdapter(new IoAdapter());
// swagger
setupSwagger(app);
// start
await app.listen(process.env.PORT || 7001, '0.0.0.0');
}
bootstrap();
================================================
FILE: src/mission/README.md
================================================
### 任务注册
在jobs下定义任务,并在`mission.module.ts`中的providers中注册即可。
### 添加任务
在后台页面中的定时任务页面中进行添加,主要识别为**服务路径**的定义:`Job类名.方法名`,填写任务参数则会在调用方法时自动传递该参数,参数会被`JSON.parse`
================================================
FILE: src/mission/jobs/http-request.job.ts
================================================
import { HttpService } from '@nestjs/axios';
import { Injectable } from '@nestjs/common';
import { LoggerService } from 'src/shared/logger/logger.service';
import { Mission } from '../mission.decorator';
/**
* Api接口请求类型任务
*/
@Injectable()
@Mission()
export class HttpRequestJob {
constructor(
private readonly httpService: HttpService,
private readonly logger: LoggerService,
) {}
/**
* 发起请求
* @param config {AxiosRequestConfig}
*/
async handle(config: unknown): Promise<void> {
if (config) {
const result = await this.httpService.axiosRef.request(config);
this.logger.log(result, HttpRequestJob.name);
} else {
throw new Error('Http request job param is empty');
}
}
}
================================================
FILE: src/mission/jobs/sys-log-clear.job.ts
================================================
import { Injectable } from '@nestjs/common';
import { SysLogService } from 'src/modules/admin/system/log/log.service';
import { Mission } from '../mission.decorator';
/**
* 管理后台日志清理任务
*/
@Injectable()
@Mission()
export class SysLogClearJob {
constructor(private sysLogService: SysLogService) {}
async clearLoginLog(): Promise<void> {
await this.sysLogService.clearLoginLog();
}
async clearTaskLog(): Promise<void> {
await this.sysLogService.clearTaskLog();
}
}
================================================
FILE: src/mission/mission.decorator.ts
================================================
import { SetMetadata } from '@nestjs/common';
import { MISSION_KEY_METADATA } from '../common/contants/decorator.contants';
/**
* 定时任务标记,没有该任务标记的任务不会被执行,保证全局获取下的模块被安全执行
*/
export const Mission = () => SetMetadata(MISSION_KEY_METADATA, true);
================================================
FILE: src/mission/mission.module.ts
================================================
import { DynamicModule, ExistingProvider, Module } from '@nestjs/common';
import { AdminModule } from 'src/modules/admin/admin.module';
import { SysLogService } from 'src/modules/admin/system/log/log.service';
import { HttpRequestJob } from './jobs/http-request.job';
import { SysLogClearJob } from './jobs/sys-log-clear.job';
const providers = [SysLogClearJob, HttpRequestJob];
/**
* auto create alias
* {
* provide: 'SysLogClearMissionService',
* useExisting: SysLogClearMissionService,
* }
*/
function createAliasProviders(): ExistingProvider[] {
const aliasProviders: ExistingProvider[] = [];
for (const p of providers) {
aliasProviders.push({
provide: p.name,
useExisting: p,
});
}
return aliasProviders;
}
/**
* 所有需要执行的定时任务都需要在这里注册
*/
@Module({})
export class MissionModule {
static forRoot(): DynamicModule {
// 使用Alias定义别名,使得可以通过字符串类型获取定义的Service,否则无法获取
const aliasProviders = createAliasProviders();
return {
global: true,
module: MissionModule,
imports: [AdminModule],
providers: [...providers, ...aliasProviders, SysLogService],
exports: aliasProviders,
};
}
}
================================================
FILE: src/modules/admin/account/account.controller.ts
================================================
import { Body, Controller, Get, Post } from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { ADMIN_PREFIX } from 'src/modules/admin/admin.constants';
import { IAdminUser } from '../admin.interface';
import { AdminUser } from '../core/decorators/admin-user.decorator';
import { PermissionOptional } from '../core/decorators/permission-optional.decorator';
import { PermMenuInfo } from '../login/login.class';
import { LoginService } from '../login/login.service';
import { AccountInfo } from '../system/user/user.class';
import { UpdatePasswordDto } from '../system/user/user.dto';
import { SysUserService } from '../system/user/user.service';
import { UpdatePersonInfoDto } from './account.dto';
@ApiTags('账户模块')
@ApiSecurity(ADMIN_PREFIX)
@Controller()
export class AccountController {
constructor(
private userService: SysUserService,
private loginService: LoginService,
) {}
@ApiOperation({ summary: '获取管理员资料' })
@ApiOkResponse({ type: AccountInfo })
@PermissionOptional()
@Get('info')
async info(@AdminUser() user: IAdminUser): Promise<AccountInfo> {
return await this.userService.getAccountInfo(user.uid);
}
@ApiOperation({ summary: '更改管理员资料' })
@PermissionOptional()
@Post('update')
async update(
@Body() dto: UpdatePersonInfoDto,
@AdminUser() user: IAdminUser,
): Promise<void> {
await this.userService.updatePersonInfo(user.uid, dto);
}
@ApiOperation({ summary: '更改管理员密码' })
@PermissionOptional()
@Post('password')
async password(
@Body() dto: UpdatePasswordDto,
@AdminUser() user: IAdminUser,
): Promise<void> {
await this.userService.updatePassword(user.uid, dto);
}
@ApiOperation({ summary: '管理员登出' })
@PermissionOptional()
@Post('logout')
async logout(@AdminUser() user: IAdminUser): Promise<void> {
await this.loginService.clearLoginStatus(user.uid);
}
@ApiOperation({ summary: '获取菜单列表及权限列表' })
@ApiOkResponse({ type: PermMenuInfo })
@PermissionOptional()
@Get('permmenu')
async permmenu(@AdminUser() user: IAdminUser): Promise<PermMenuInfo> {
return await this.loginService.getPermMenu(user.uid);
}
}
================================================
FILE: src/modules/admin/account/account.dto.ts
================================================
import { Optional } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
export class UpdatePersonInfoDto {
@ApiProperty({ description: '管理员昵称', required: false })
@IsString()
@Optional()
nickName: string;
@ApiProperty({ description: '邮箱', required: false })
@IsString()
@Optional()
email: string;
@ApiProperty({ description: '手机', required: false })
@IsString()
@Optional()
phone: string;
@ApiProperty({ description: '备注', required: false })
@IsString()
@Optional()
remark: string;
}
================================================
FILE: src/modules/admin/account/account.module.ts
================================================
import { Module } from '@nestjs/common';
import { LoginModule } from '../login/login.module';
import { SystemModule } from '../system/system.module';
import { AccountController } from './account.controller';
@Module({
imports: [SystemModule, LoginModule],
controllers: [AccountController],
})
export class AccountModule {}
================================================
FILE: src/modules/admin/admin.constants.ts
================================================
export const ADMIN_USER = 'adminUser';
export const AUTHORIZE_KEY_METADATA = 'admin_module:authorize';
export const PERMISSION_OPTIONAL_KEY_METADATA =
'admin_module:permission_optional';
export const LOG_DISABLED_KEY_METADATA = 'admin_module:log_disabled';
export const ROOT_ROLE_ID = 'admin_module:root_role_id';
export const QINIU_CONFIG = 'admin_module:qiniu_config';
export const SYS_TASK_QUEUE_NAME = 'admin_module:sys-task';
export const SYS_TASK_QUEUE_PREFIX = 'admin:sys:task';
export const FORBIDDEN_OP_MENU_ID_INDEX = 91;
export const ADMIN_PREFIX = 'admin';
export const QINIU_API = 'http://api.qiniu.com';
export const NETDISK_TASK_PREFIX = 'admin:netdisk:';
// 目录分隔符
export const NETDISK_DELIMITER = '/';
export const NETDISK_LIMIT = 100;
export const NETDISK_HANDLE_MAX_ITEM = 1000;
export const NETDISK_COPY_SUFFIX = '的副本';
================================================
FILE: src/modules/admin/admin.interface.ts
================================================
import { conf } from 'qiniu';
export interface IAdminUser {
uid: number;
pv: number;
}
export type QINIU_ACCESS_CONTROL = 'private' | 'public';
export interface IQiniuConfig {
accessKey: string;
secretKey: string;
bucket: string;
zone: conf.Zone;
domain: string;
access: QINIU_ACCESS_CONTROL;
}
================================================
FILE: src/modules/admin/admin.module.ts
================================================
import { Module } from '@nestjs/common';
import { APP_GUARD, RouterModule } from '@nestjs/core';
import { AccountModule } from './account/account.module';
import { ADMIN_PREFIX } from './admin.constants';
import { AuthGuard } from './core/guards/auth.guard';
import { LoginModule } from './login/login.module';
import { NetdiskModule } from './netdisk/netdisk.module';
import { SystemModule } from './system/system.module';
/**
* Admin模块,所有API都需要加入/admin前缀
*/
@Module({
imports: [
// register prefix
RouterModule.register([
{
path: ADMIN_PREFIX,
children: [
{ path: 'netdisk', module: NetdiskModule },
{ path: 'account', module: AccountModule },
{ path: 'sys', module: SystemModule },
],
},
// like this url /admin/captcha/img
{
path: ADMIN_PREFIX,
module: LoginModule,
},
]),
// component module
LoginModule,
SystemModule,
AccountModule,
NetdiskModule,
],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
exports: [SystemModule],
})
export class AdminModule {}
================================================
FILE: src/modules/admin/core/decorators/admin-user.decorator.ts
================================================
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { ADMIN_USER } from '../../admin.constants';
export const AdminUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
// auth guard will mount this
const user = request[ADMIN_USER];
return data ? user?.[data] : user;
},
);
================================================
FILE: src/modules/admin/core/decorators/authorize.decorator.ts
================================================
import { SetMetadata } from '@nestjs/common';
import { AUTHORIZE_KEY_METADATA } from '../../admin.constants';
/**
* 开放授权Api,使用该注解则无需校验Token及权限
*/
export const Authorize = () => SetMetadata(AUTHORIZE_KEY_METADATA, true);
================================================
FILE: src/modules/admin/core/decorators/log-disabled.decorator.ts
================================================
import { SetMetadata } from '@nestjs/common';
import { LOG_DISABLED_KEY_METADATA } from '../../admin.constants';
/**
* 日志记录禁用
*/
export const LogDisabled = () => SetMetadata(LOG_DISABLED_KEY_METADATA, true);
================================================
FILE: src/modules/admin/core/decorators/permission-optional.decorator.ts
================================================
import { SetMetadata } from '@nestjs/common';
import { PERMISSION_OPTIONAL_KEY_METADATA } from '../../admin.constants';
/**
* 使用该注解可开放当前Api权限,无需权限访问,但是仍然需要校验身份Token
*/
export const PermissionOptional = () =>
SetMetadata(PERMISSION_OPTIONAL_KEY_METADATA, true);
================================================
FILE: src/modules/admin/core/guards/auth.guard.ts
================================================
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { FastifyRequest } from 'fastify';
import { isEmpty } from 'lodash';
import { JwtService } from '@nestjs/jwt';
import { ApiException } from 'src/common/exceptions/api.exception';
import {
ADMIN_PREFIX,
ADMIN_USER,
PERMISSION_OPTIONAL_KEY_METADATA,
AUTHORIZE_KEY_METADATA,
} from 'src/modules/admin/admin.constants';
import { LoginService } from 'src/modules/admin/login/login.service';
/**
* admin perm check guard
*/
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private jwtService: JwtService,
private loginService: LoginService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 检测是否是开放类型的,例如获取验证码类型的接口不需要校验,可以加入@Authorize可自动放过
const authorize = this.reflector.get<boolean>(
AUTHORIZE_KEY_METADATA,
context.getHandler(),
);
if (authorize) {
return true;
}
const request = context.switchToHttp().getRequest<FastifyRequest>();
const url = request.url;
const path = url.split('?')[0];
const token = request.headers['authorization'] as string;
if (isEmpty(token)) {
throw new ApiException(11001);
}
try {
// 挂载对象到当前请求上
request[ADMIN_USER] = this.jwtService.verify(token);
} catch (e) {
// 无法通过token校验
throw new ApiException(11001);
}
if (isEmpty(request[ADMIN_USER])) {
throw new ApiException(11001);
}
const pv = await this.loginService.getRedisPasswordVersionById(
request[ADMIN_USER].uid,
);
if (pv !== `${request[ADMIN_USER].pv}`) {
// 密码版本不一致,登录期间已更改过密码
throw new ApiException(11002);
}
const redisToken = await this.loginService.getRedisTokenById(
request[ADMIN_USER].uid,
);
if (token !== redisToken) {
// 与redis保存不一致
throw new ApiException(11002);
}
// 注册该注解,Api则放行检测
const notNeedPerm = this.reflector.get<boolean>(
PERMISSION_OPTIONAL_KEY_METADATA,
context.getHandler(),
);
// Token校验身份通过,判断是否需要权限的url,不需要权限则pass
if (notNeedPerm) {
return true;
}
const perms: string = await this.loginService.getRedisPermsById(
request[ADMIN_USER].uid,
);
// 安全判空
if (isEmpty(perms)) {
throw new ApiException(11001);
}
// 将sys:admin:user等转换成sys/admin/user
const permArray: string[] = (JSON.parse(perms) as string[]).map((e) => {
return e.replace(/:/g, '/');
});
// 遍历权限是否包含该url,不包含则无访问权限
if (!permArray.includes(path.replace(`/${ADMIN_PREFIX}/`, ''))) {
throw new ApiException(11003);
}
// pass
return true;
}
}
================================================
FILE: src/modules/admin/core/provider/qiniu.provider.ts
================================================
import { FactoryProvider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { QINIU_CONFIG } from 'src/modules/admin/admin.constants';
import { IQiniuConfig } from '../../admin.interface';
/**
* 提供使用 @Inject(QINIU_CONFIG) 直接获取七牛配置
*/
export function qiniuProvider(): FactoryProvider {
return {
provide: QINIU_CONFIG,
useFactory: (configService: ConfigService): IQiniuConfig => ({
accessKey: configService.get('qiniu.accessKey'),
secretKey: configService.get('qiniu.secretKey'),
domain: configService.get('qiniu.domain'),
bucket: configService.get('qiniu.bucket'),
zone: configService.get('qiniu.zone'),
access: configService.get('qiniu.access'),
}),
inject: [ConfigService],
};
}
================================================
FILE: src/modules/admin/core/provider/root-role-id.provider.ts
================================================
import { FactoryProvider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';
/**
* 提供使用 @Inject(ROOT_ROLE_ID) 直接获取RootRoleId
*/
export function rootRoleIdProvider(): FactoryProvider {
return {
provide: ROOT_ROLE_ID,
useFactory: (configService: ConfigService) => {
return configService.get<number>('rootRoleId', 1);
},
inject: [ConfigService],
};
}
================================================
FILE: src/modules/admin/login/login.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import SysMenu from 'src/entities/admin/sys-menu.entity';
export class ImageCaptcha {
@ApiProperty({
description: 'base64格式的svg图片',
})
img: string;
@ApiProperty({
description: '验证码对应的唯一ID',
})
id: string;
}
export class LoginToken {
@ApiProperty({ description: 'JWT身份Token' })
token: string;
}
export class PermMenuInfo {
@ApiProperty({ description: '菜单列表', type: [SysMenu] })
menus: SysMenu[];
@ApiProperty({ description: '权限列表', type: [String] })
perms: string[];
}
================================================
FILE: src/modules/admin/login/login.controller.ts
================================================
import {
Body,
Controller,
Get,
Headers,
Post,
Query,
Req,
} from '@nestjs/common';
import { FastifyRequest } from 'fastify';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Authorize } from '../core/decorators/authorize.decorator';
import { ImageCaptchaDto, LoginInfoDto } from './login.dto';
import { ImageCaptcha, LoginToken } from './login.class';
import { LoginService } from './login.service';
import { LogDisabled } from '../core/decorators/log-disabled.decorator';
import { UtilService } from 'src/shared/services/util.service';
@ApiTags('登录模块')
@Controller()
export class LoginController {
constructor(private loginService: LoginService, private utils: UtilService) {}
@ApiOperation({
summary: '获取登录图片验证码',
})
@ApiOkResponse({ type: ImageCaptcha })
@Get('captcha/img')
@Authorize()
async captchaByImg(@Query() dto: ImageCaptchaDto): Promise<ImageCaptcha> {
return await this.loginService.createImageCaptcha(dto);
}
@ApiOperation({
summary: '管理员登录',
})
@ApiOkResponse({ type: LoginToken })
@Post('login')
@LogDisabled()
@Authorize()
async login(
@Body() dto: LoginInfoDto,
@Req() req: FastifyRequest,
@Headers('user-agent') ua: string,
): Promise<LoginToken> {
await this.loginService.checkImgCaptcha(dto.captchaId, dto.verifyCode);
const token = await this.loginService.getLoginSign(
dto.username,
dto.password,
this.utils.getReqIP(req),
ua,
);
return { token };
}
}
================================================
FILE: src/modules/admin/login/login.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsInt,
IsOptional,
IsString,
MaxLength,
MinLength,
} from 'class-validator';
export class ImageCaptchaDto {
@ApiProperty({
required: false,
default: 100,
description: '验证码宽度',
})
@Type(() => Number)
@IsInt()
@IsOptional()
readonly width: number = 100;
@ApiProperty({
required: false,
default: 50,
description: '验证码宽度',
})
@Type(() => Number)
@IsInt()
@IsOptional()
readonly height: number = 50;
}
export class LoginInfoDto {
@ApiProperty({ description: '管理员用户名' })
@IsString()
@MinLength(1)
username: string;
@ApiProperty({ description: '管理员密码' })
@IsString()
@MinLength(4)
password: string;
@ApiProperty({ description: '验证码标识' })
@IsString()
captchaId: string;
@ApiProperty({ description: '用户输入的验证码' })
@IsString()
@MinLength(4)
@MaxLength(4)
verifyCode: string;
}
================================================
FILE: src/modules/admin/login/login.module.ts
================================================
import { Module } from '@nestjs/common';
import { SystemModule } from '../system/system.module';
import { LoginController } from './login.controller';
import { LoginService } from './login.service';
@Module({
imports: [SystemModule],
controllers: [LoginController],
providers: [LoginService],
exports: [LoginService],
})
export class LoginModule {}
================================================
FILE: src/modules/admin/login/login.service.ts
================================================
import { Injectable } from '@nestjs/common';
import * as svgCaptcha from 'svg-captcha';
import { ImageCaptcha, PermMenuInfo } from './login.class';
import { isEmpty } from 'lodash';
import { ImageCaptchaDto } from './login.dto';
import { JwtService } from '@nestjs/jwt';
import { UtilService } from 'src/shared/services/util.service';
import { SysMenuService } from '../system/menu/menu.service';
import { SysUserService } from '../system/user/user.service';
import { ApiException } from 'src/common/exceptions/api.exception';
import { SysLogService } from '../system/log/log.service';
import { RedisService } from 'src/shared/services/redis.service';
@Injectable()
export class LoginService {
constructor(
private redisService: RedisService,
private menuService: SysMenuService,
private userService: SysUserService,
private logService: SysLogService,
private util: UtilService,
private jwtService: JwtService,
) {}
/**
* 创建验证码并缓存加入redis缓存
* @param captcha 验证码长宽
* @returns svg & id obj
*/
async createImageCaptcha(captcha: ImageCaptchaDto): Promise<ImageCaptcha> {
const svg = svgCaptcha.create({
size: 4,
color: true,
noise: 4,
width: isEmpty(captcha.width) ? 100 : captcha.width,
height: isEmpty(captcha.height) ? 50 : captcha.height,
charPreset: '1234567890',
});
const result = {
img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString(
'base64',
)}`,
id: this.util.generateUUID(), // this.utils.generateUUID()
};
// 5分钟过期时间
await this.redisService
.getRedis()
.set(`admin:captcha:img:${result.id}`, svg.text, 'EX', 60 * 5);
return result;
}
/**
* 校验验证码
*/
async checkImgCaptcha(id: string, code: string): Promise<void> {
const result = await this.redisService
.getRedis()
.get(`admin:captcha:img:${id}`);
if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase()) {
throw new ApiException(10002);
}
// 校验成功后移除验证码
await this.redisService.getRedis().del(`admin:captcha:img:${id}`);
}
/**
* 获取登录JWT
* 返回null则账号密码有误,不存在该用户
*/
async getLoginSign(
username: string,
password: string,
ip: string,
ua: string,
): Promise<string> {
const user = await this.userService.findUserByUserName(username);
if (isEmpty(user)) {
throw new ApiException(10003);
}
const comparePassword = this.util.md5(`${password}${user.psalt}`);
if (user.password !== comparePassword) {
throw new ApiException(10003);
}
const perms = await this.menuService.getPerms(user.id);
const jwtSign = this.jwtService.sign(
{
uid: parseInt(user.id.toString()),
pv: 1,
},
// {
// expiresIn: '24h',
// },
);
await this.redisService
.getRedis()
.set(`admin:passwordVersion:${user.id}`, 1);
// Token设置过期时间 24小时
await this.redisService
.getRedis()
.set(`admin:token:${user.id}`, jwtSign, 'EX', 60 * 60 * 24);
await this.redisService
.getRedis()
.set(`admin:perms:${user.id}`, JSON.stringify(perms));
await this.logService.saveLoginLog(user.id, ip, ua);
return jwtSign;
}
/**
* 清除登录状态信息
*/
async clearLoginStatus(uid: number): Promise<void> {
await this.userService.forbidden(uid);
}
/**
* 获取权限菜单
*/
async getPermMenu(uid: number): Promise<PermMenuInfo> {
const menus = await this.menuService.getMenus(uid);
const perms = await this.menuService.getPerms(uid);
return { menus, perms };
}
async getRedisPasswordVersionById(id: number): Promise<string> {
return this.redisService.getRedis().get(`admin:passwordVersion:${id}`);
}
async getRedisTokenById(id: number): Promise<string> {
return this.redisService.getRedis().get(`admin:token:${id}`);
}
async getRedisPermsById(id: number): Promise<string> {
return this.redisService.getRedis().get(`admin:perms:${id}`);
}
}
================================================
FILE: src/modules/admin/netdisk/manager/manage.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
export type FileType = 'file' | 'dir';
export class SFileInfo {
@ApiProperty({ description: '文件id' })
id: string;
@ApiProperty({ description: '文件类型', enum: ['file', 'dir'] })
type: FileType;
@ApiProperty({ description: '文件名称' })
name: string;
@ApiProperty({ description: '存入时间', type: Date })
putTime?: Date;
@ApiProperty({ description: '文件大小, byte单位' })
fsize?: string;
@ApiProperty({ description: '文件的mime-type' })
mimeType?: string;
@ApiProperty({ description: '所属目录' })
belongTo?: string;
}
export class SFileList {
@ApiProperty({ description: '文件列表', type: [SFileInfo] })
list: SFileInfo[];
@ApiProperty({ description: '分页标志,空则代表加载完毕' })
marker?: string;
}
export class UploadToken {
@ApiProperty({ description: '上传token' })
token: string;
}
export class SFileInfoDetail {
@ApiProperty({ description: '文件大小,int64类型,单位为字节(Byte)' })
fsize: number;
@ApiProperty({ description: '文件HASH值' })
hash: string;
@ApiProperty({ description: '文件MIME类型,string类型' })
mimeType: string;
@ApiProperty({
description:
'文件存储类型,2 表示归档存储,1 表示低频存储,0表示普通存储。',
})
type: number;
@ApiProperty({ description: '文件上传时间', type: Date })
putTime: Date;
@ApiProperty({ description: '文件md5值' })
md5: string;
@ApiProperty({ description: '上传人' })
uploader: string;
@ApiProperty({ description: '文件备注' })
mark?: string;
}
================================================
FILE: src/modules/admin/netdisk/manager/manage.controller.ts
================================================
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { NetDiskManageService } from './manage.service';
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import {
DeleteDto,
FileInfoDto,
FileOpDto,
GetFileListDto,
MarkFileDto,
MKDirDto,
RenameDto,
} from './manage.dto';
import { SFileInfoDetail, SFileList, UploadToken } from './manage.class';
import { ApiException } from 'src/common/exceptions/api.exception';
import { AdminUser } from '../../core/decorators/admin-user.decorator';
import { IAdminUser } from '../../admin.interface';
import { ADMIN_PREFIX } from '../../admin.constants';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('网盘管理模块')
@Controller('manage')
export class NetDiskManageController {
constructor(private manageService: NetDiskManageService) {}
@ApiOperation({ summary: '获取文件列表' })
@ApiOkResponse({ type: SFileList })
@Get('list')
async list(@Query() dto: GetFileListDto): Promise<SFileList> {
return await this.manageService.getFileList(dto.path, dto.marker, dto.key);
}
@ApiOperation({ summary: '创建文件夹,支持多级' })
@Post('mkdir')
async mkdir(@Body() dto: MKDirDto): Promise<void> {
const result = await this.manageService.checkFileExist(
`${dto.path}${dto.dirName}/`,
);
if (result) {
throw new ApiException(20001);
}
await this.manageService.createDir(`${dto.path}${dto.dirName}`);
}
@ApiOperation({ summary: '获取上传Token,无Token前端无法上传' })
@ApiOkResponse({ type: UploadToken })
@Get('token')
async token(@AdminUser() user: IAdminUser): Promise<UploadToken> {
return {
token: this.manageService.createUploadToken(`${user.uid}`),
};
}
@ApiOperation({ summary: '获取文件详细信息' })
@ApiOkResponse({ type: SFileInfoDetail })
@Post('info')
async info(@Body() dto: FileInfoDto): Promise<SFileInfoDetail> {
return await this.manageService.getFileInfo(dto.name, dto.path);
}
@ApiOperation({ summary: '添加文件备注' })
@Post('mark')
async mark(@Body() dto: MarkFileDto): Promise<void> {
await this.manageService.changeFileHeaders(dto.name, dto.path, {
mark: dto.mark,
});
}
@ApiOperation({ summary: '获取下载链接,不支持下载文件夹' })
@ApiOkResponse({ type: String })
@Post('download')
async download(@Body() dto: FileInfoDto): Promise<string> {
return this.manageService.getDownloadLink(`${dto.path}${dto.name}`);
}
@ApiOperation({ summary: '重命名文件或文件夹' })
@Post('rename')
async rename(@Body() dto: RenameDto): Promise<void> {
const result = await this.manageService.checkFileExist(
`${dto.path}${dto.toName}${dto.type === 'dir' ? '/' : ''}`,
);
if (result) {
throw new ApiException(20001);
}
if (dto.type === 'file') {
await this.manageService.renameFile(dto.path, dto.name, dto.toName);
} else {
await this.manageService.renameDir(dto.path, dto.name, dto.toName);
}
}
@ApiOperation({ summary: '删除文件或文件夹' })
@Post('delete')
async delete(@Body() dto: DeleteDto): Promise<void> {
await this.manageService.deleteMultiFileOrDir(dto.files, dto.path);
}
@ApiOperation({ summary: '剪切文件或文件夹,支持批量' })
@Post('cut')
async cut(@Body() dto: FileOpDto): Promise<void> {
if (dto.originPath === dto.toPath) {
throw new ApiException(20002);
}
await this.manageService.moveMultiFileOrDir(
dto.files,
dto.originPath,
dto.toPath,
);
}
@ApiOperation({ summary: '复制文件或文件夹,支持批量' })
@Post('copy')
async copy(@Body() dto: FileOpDto): Promise<void> {
await this.manageService.copyMultiFileOrDir(
dto.files,
dto.originPath,
dto.toPath,
);
}
}
================================================
FILE: src/modules/admin/netdisk/manager/manage.dto.ts
================================================
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMaxSize,
IsNotEmpty,
IsOptional,
IsString,
Matches,
Validate,
ValidateIf,
ValidateNested,
ValidationArguments,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
import { isEmpty } from 'lodash';
import { NETDISK_HANDLE_MAX_ITEM } from '../../admin.constants';
@ValidatorConstraint({ name: 'IsLegalNameExpression', async: false })
export class IsLegalNameExpression implements ValidatorConstraintInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validate(value: string, args: ValidationArguments) {
try {
if (isEmpty(value)) {
throw new Error('dir name is empty');
}
if (value.includes('/')) {
throw new Error('dir name not allow /');
}
return true;
} catch (e) {
return false;
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
defaultMessage(_args: ValidationArguments) {
// here you can provide default error message if validation failed
return 'file or dir name invalid';
}
}
export class FileOpItem {
@ApiProperty({ description: '文件类型', enum: ['file', 'dir'] })
@IsString()
@Matches(/(^file$)|(^dir$)/)
type: string;
@ApiProperty({ description: '文件名称' })
@IsString()
@IsNotEmpty()
@Validate(IsLegalNameExpression)
name: string;
}
export class GetFileListDto {
@ApiProperty({ description: '分页标识' })
@IsOptional()
@IsString()
marker: string;
@ApiProperty({ description: '当前路径' })
@IsString()
path: string;
@ApiPropertyOptional({ description: '搜索关键字' })
@Validate(IsLegalNameExpression)
@ValidateIf((o) => !isEmpty(o.key))
@IsString()
key: string;
}
export class MKDirDto {
@ApiProperty({ description: '文件夹名称' })
@IsNotEmpty()
@IsString()
@Validate(IsLegalNameExpression)
dirName: string;
@ApiProperty({ description: '所属路径' })
@IsString()
path: string;
}
export class RenameDto {
@ApiProperty({ description: '文件类型' })
@IsString()
@Matches(/(^file$)|(^dir$)/)
type: string;
@ApiProperty({ description: '更改的名称' })
@IsString()
@IsNotEmpty()
@Validate(IsLegalNameExpression)
toName: string;
@ApiProperty({ description: '原来的名称' })
@IsString()
@IsNotEmpty()
@Validate(IsLegalNameExpression)
name: string;
@ApiProperty({ description: '路径' })
@IsString()
path: string;
}
export class FileInfoDto {
@ApiProperty({ description: '文件名' })
@IsString()
@IsNotEmpty()
@Validate(IsLegalNameExpression)
name: string;
@ApiProperty({ description: '文件所在路径' })
@IsString()
path: string;
}
export class DeleteDto {
@ApiProperty({ description: '需要操作的文件或文件夹', type: [FileOpItem] })
@Type(() => FileOpItem)
@ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)
@ValidateNested({ each: true })
files: FileOpItem[];
@ApiProperty({ description: '所在目录' })
@IsString()
path: string;
}
export class MarkFileDto {
@ApiProperty({ description: '文件名' })
@IsString()
@IsNotEmpty()
@Validate(IsLegalNameExpression)
name: string;
@ApiProperty({ description: '文件所在路径' })
@IsString()
path: string;
@ApiProperty({ description: '备注信息' })
@IsString()
mark: string;
}
export class FileOpDto {
@ApiProperty({ description: '需要操作的文件或文件夹', type: [FileOpItem] })
@Type(() => FileOpItem)
@ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)
@ValidateNested({ each: true })
files: FileOpItem[];
@ApiProperty({ description: '操作前的目录' })
@IsString()
originPath: string;
@ApiProperty({ description: '操作后的目录' })
@IsString()
toPath: string;
}
================================================
FILE: src/modules/admin/netdisk/manager/manage.service.ts
================================================
import { Inject, Injectable } from '@nestjs/common';
import {
NETDISK_COPY_SUFFIX,
NETDISK_DELIMITER,
NETDISK_HANDLE_MAX_ITEM,
NETDISK_LIMIT,
QINIU_CONFIG,
} from '../../admin.constants';
import { IQiniuConfig } from '../../admin.interface';
import * as qiniu from 'qiniu';
import { rs, conf, auth } from 'qiniu';
import { UtilService } from 'src/shared/services/util.service';
import { isEmpty } from 'lodash';
import { SFileInfo, SFileInfoDetail, SFileList } from './manage.class';
import { SysUserService } from '../../system/user/user.service';
import { AccountInfo } from '../../system/user/user.class';
import { extname, basename } from 'path';
import { FileOpItem } from './manage.dto';
@Injectable()
export class NetDiskManageService {
private config: conf.ConfigOptions;
private mac: auth.digest.Mac;
private bucketManager: rs.BucketManager;
constructor(
@Inject(QINIU_CONFIG) private qiniuConfig: IQiniuConfig,
private userService: SysUserService,
private util: UtilService,
) {
this.mac = new qiniu.auth.digest.Mac(
this.qiniuConfig.accessKey,
this.qiniuConfig.secretKey,
);
this.config = new qiniu.conf.Config({
zone: this.qiniuConfig.zone,
});
// bucket manager
this.bucketManager = new qiniu.rs.BucketManager(this.mac, this.config);
}
/**
* 获取文件列表
* @param prefix 当前文件夹路径,搜索模式下会被忽略
* @param marker 下一页标识
* @returns iFileListResult
*/
async getFileList(prefix = '', marker = '', skey = ''): Promise<SFileList> {
// 是否需要搜索
const searching = !isEmpty(skey);
return new Promise<SFileList>((resolve, reject) => {
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: searching ? '' : prefix,
limit: NETDISK_LIMIT,
delimiter: searching ? '' : NETDISK_DELIMITER,
marker,
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
// 如果这个nextMarker不为空,那么还有未列举完毕的文件列表,下次调用listPrefix的时候,
// 指定options里面的marker为这个值
const fileList: SFileInfo[] = [];
// 处理目录,但只有非搜索模式下可用
if (!searching && !isEmpty(respBody.commonPrefixes)) {
// dir
for (const dirPath of respBody.commonPrefixes) {
const name = (dirPath as string)
.substr(0, dirPath.length - 1)
.replace(prefix, '');
if (isEmpty(skey) || name.includes(skey)) {
fileList.push({
name: (dirPath as string)
.substr(0, dirPath.length - 1)
.replace(prefix, ''),
type: 'dir',
id: this.util.generateRandomValue(10),
});
}
}
}
// handle items
if (!isEmpty(respBody.items)) {
// file
for (const item of respBody.items) {
// 搜索模式下处理
if (searching) {
const pathList: string[] = item.key.split(NETDISK_DELIMITER);
// dir is empty stirng, file is key string
const name = pathList.pop();
if (
item.key.endsWith(NETDISK_DELIMITER) &&
pathList[pathList.length - 1].includes(skey)
) {
// 结果是目录
const ditName = pathList.pop();
fileList.push({
id: this.util.generateRandomValue(10),
name: ditName,
type: 'dir',
belongTo: pathList.join(NETDISK_DELIMITER),
});
} else if (name.includes(skey)) {
// 文件
fileList.push({
id: this.util.generateRandomValue(10),
name,
type: 'file',
fsize: item.fsize,
mimeType: item.mimeType,
putTime: new Date(parseInt(item.putTime) / 10000),
belongTo: pathList.join(NETDISK_DELIMITER),
});
}
} else {
// 正常获取列表
const fileKey = item.key.replace(prefix, '') as string;
if (!isEmpty(fileKey)) {
fileList.push({
id: this.util.generateRandomValue(10),
name: fileKey,
type: 'file',
fsize: item.fsize,
mimeType: item.mimeType,
putTime: new Date(parseInt(item.putTime) / 10000),
});
}
}
}
}
resolve({
list: fileList,
marker: respBody.marker || null,
});
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
/**
* 获取文件信息
*/
async getFileInfo(name: string, path: string): Promise<SFileInfoDetail> {
return new Promise((resolve, reject) => {
this.bucketManager.stat(
this.qiniuConfig.bucket,
`${path}${name}`,
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode == 200) {
const detailInfo: SFileInfoDetail = {
fsize: respBody.fsize,
hash: respBody.hash,
md5: respBody.md5,
mimeType: respBody.mimeType.split('/x-qn-meta')[0],
putTime: new Date(parseInt(respBody.putTime) / 10000),
type: respBody.type,
uploader: '',
mark: respBody?.['x-qn-meta']?.['!mark'] ?? '',
};
if (!respBody.endUser) {
resolve(detailInfo);
} else {
this.userService
.getAccountInfo(parseInt(respBody.endUser))
.then((user: AccountInfo) => {
if (isEmpty(user)) {
resolve(detailInfo);
} else {
detailInfo.uploader = user.name;
resolve(detailInfo);
}
});
}
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
/**
* 修改文件MimeType
*/
async changeFileHeaders(
name: string,
path: string,
headers: { [k: string]: string },
): Promise<void> {
return new Promise((resolve, reject) => {
this.bucketManager.changeHeaders(
this.qiniuConfig.bucket,
`${path}${name}`,
headers,
(err, _, respInfo) => {
if (err) {
reject();
return;
}
if (respInfo.statusCode == 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
/**
* 创建文件夹
* @returns true创建成功
*/
async createDir(dirName: string): Promise<void> {
const safeDirName = dirName.endsWith('/') ? dirName : `${dirName}/`;
return new Promise((resolve, reject) => {
// 上传一个空文件以用于显示文件夹效果
const formUploader = new qiniu.form_up.FormUploader(this.config);
const putExtra = new qiniu.form_up.PutExtra();
formUploader.put(
this.createUploadToken(''),
safeDirName,
' ',
putExtra,
(respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
/**
* 检查文件是否存在,同可检查目录
*/
async checkFileExist(filePath: string): Promise<boolean> {
return new Promise((resolve, reject) => {
// fix path end must a /
// 检测文件夹是否存在
this.bucketManager.stat(
this.qiniuConfig.bucket,
filePath,
(respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr);
return;
}
if (respInfo.statusCode === 200) {
// 文件夹存在
resolve(true);
} else if (respInfo.statusCode === 612) {
// 文件夹不存在
resolve(false);
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
/**
* 创建Upload Token, 默认过期时间一小时
* @returns upload token
*/
createUploadToken(endUser: string): string {
const policy = new qiniu.rs.PutPolicy({
scope: this.qiniuConfig.bucket,
insertOnly: 1,
endUser,
});
const uploadToken = policy.uploadToken(this.mac);
return uploadToken;
}
/**
* 重命名文件
* @param dir 文件路径
* @param name 文件名称
*/
async renameFile(dir: string, name: string, toName: string): Promise<void> {
const fileName = `${dir}${name}`;
const toFileName = `${dir}${toName}`;
const op = {
force: true,
};
return new Promise((resolve, reject) => {
this.bucketManager.move(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
(err, respBody, respInfo) => {
if (err) {
reject(err);
} else {
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
}
},
);
});
}
/**
* 移动文件
*/
async moveFile(dir: string, toDir: string, name: string): Promise<void> {
const fileName = `${dir}${name}`;
const toFileName = `${toDir}${name}`;
const op = {
force: true,
};
return new Promise((resolve, reject) => {
this.bucketManager.move(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
(err, respBody, respInfo) => {
if (err) {
reject(err);
} else {
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
}
},
);
});
}
/**
* 复制文件
*/
async copyFile(dir: string, toDir: string, name: string): Promise<void> {
const fileName = `${dir}${name}`;
// 拼接文件名
const ext = extname(name);
const bn = basename(name, ext);
const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`;
const op = {
force: true,
};
return new Promise((resolve, reject) => {
this.bucketManager.copy(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
(err, respBody, respInfo) => {
if (err) {
reject(err);
} else {
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
}
},
);
});
}
/**
* 重命名文件夹
*/
async renameDir(path: string, name: string, toName: string): Promise<void> {
const dirName = `${path}${name}`;
const toDirName = `${path}${toName}`;
let hasFile = true;
let marker = '';
const op = {
force: true,
};
const bucketName = this.qiniuConfig.bucket;
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker,
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map((item) => {
const { key } = item;
const destKey = key.replace(dirName, toDirName);
return qiniu.rs.moveOp(
bucketName,
key,
bucketName,
destKey,
op,
);
});
this.bucketManager.batch(
moveOperations,
(err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) {
hasFile = false;
} else {
marker = respBody.marker;
}
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,
),
);
}
},
);
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
}
/**
* 获取七牛下载的文件url链接
* @param key 文件路径
* @returns 连接
*/
getDownloadLink(key: string): string {
if (this.qiniuConfig.access === 'public') {
return this.bucketManager.publicDownloadUrl(this.qiniuConfig.domain, key);
} else if (this.qiniuConfig.access === 'private') {
return this.bucketManager.privateDownloadUrl(
this.qiniuConfig.domain,
key,
Date.now() / 1000 + 36000,
);
}
throw new Error('qiniu config access type not support');
}
/**
* 删除文件
* @param dir 删除的文件夹目录
* @param name 文件名
*/
async deleteFile(dir: string, name: string): Promise<void> {
return new Promise((resolve, reject) => {
this.bucketManager.delete(
this.qiniuConfig.bucket,
`${dir}${name}`,
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
/**
* 删除文件夹
* @param dir 文件夹所在的上级目录
* @param name 文件目录名称
*/
async deleteMultiFileOrDir(
fileList: FileOpItem[],
dir: string,
): Promise<void> {
const files = fileList.filter((item) => item.type === 'file');
if (files.length > 0) {
// 批处理文件
const copyOperations = files.map((item) => {
const fileName = `${dir}${item.name}`;
return qiniu.rs.deleteOp(this.qiniuConfig.bucket, fileName);
});
await new Promise<void>((resolve, reject) => {
this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else if (respInfo.statusCode === 298) {
reject(new Error('操作异常,但部分文件夹删除成功'));
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
});
});
}
// 处理文件夹
const dirs = fileList.filter((item) => item.type === 'dir');
if (dirs.length > 0) {
// 处理文件夹的复制
for (let i = 0; i < dirs.length; i++) {
const dirName = `${dir}${dirs[i].name}/`;
let hasFile = true;
let marker = '';
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker,
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map((item) => {
const { key } = item;
return qiniu.rs.deleteOp(this.qiniuConfig.bucket, key);
});
this.bucketManager.batch(
moveOperations,
(err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) {
hasFile = false;
} else {
marker = respBody.marker;
}
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,
),
);
}
},
);
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
}
}
}
/**
* 复制文件,含文件夹
*/
async copyMultiFileOrDir(
fileList: FileOpItem[],
dir: string,
toDir: string,
): Promise<void> {
const files = fileList.filter((item) => item.type === 'file');
const op = {
force: true,
};
if (files.length > 0) {
// 批处理文件
const copyOperations = files.map((item) => {
const fileName = `${dir}${item.name}`;
// 拼接文件名
const ext = extname(item.name);
const bn = basename(item.name, ext);
const toFileName = `${toDir}${bn}${NETDISK_COPY_SUFFIX}${ext}`;
return qiniu.rs.copyOp(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
);
});
await new Promise<void>((resolve, reject) => {
this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else if (respInfo.statusCode === 298) {
reject(new Error('操作异常,但部分文件夹删除成功'));
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
});
});
}
// 处理文件夹
const dirs = fileList.filter((item) => item.type === 'dir');
if (dirs.length > 0) {
// 处理文件夹的复制
for (let i = 0; i < dirs.length; i++) {
const dirName = `${dir}${dirs[i].name}/`;
const copyDirName = `${toDir}${dirs[i].name}${NETDISK_COPY_SUFFIX}/`;
let hasFile = true;
let marker = '';
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker,
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map((item) => {
const { key } = item;
const destKey = key.replace(dirName, copyDirName);
return qiniu.rs.copyOp(
this.qiniuConfig.bucket,
key,
this.qiniuConfig.bucket,
destKey,
op,
);
});
this.bucketManager.batch(
moveOperations,
(err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) {
hasFile = false;
} else {
marker = respBody.marker;
}
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,
),
);
}
},
);
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
}
}
}
/**
* 移动文件,含文件夹
*/
async moveMultiFileOrDir(
fileList: FileOpItem[],
dir: string,
toDir: string,
): Promise<void> {
const files = fileList.filter((item) => item.type === 'file');
const op = {
force: true,
};
if (files.length > 0) {
// 批处理文件
const copyOperations = files.map((item) => {
const fileName = `${dir}${item.name}`;
const toFileName = `${toDir}${item.name}`;
return qiniu.rs.moveOp(
this.qiniuConfig.bucket,
fileName,
this.qiniuConfig.bucket,
toFileName,
op,
);
});
await new Promise<void>((resolve, reject) => {
this.bucketManager.batch(copyOperations, (err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
resolve();
} else if (respInfo.statusCode === 298) {
reject(new Error('操作异常,但部分文件夹删除成功'));
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
});
});
}
// 处理文件夹
const dirs = fileList.filter((item) => item.type === 'dir');
if (dirs.length > 0) {
// 处理文件夹的复制
for (let i = 0; i < dirs.length; i++) {
const dirName = `${dir}${dirs[i].name}/`;
const toDirName = `${toDir}${dirs[i].name}/`;
// 移动的目录不是是自己
if (toDirName.startsWith(dirName)) {
continue;
}
let hasFile = true;
let marker = '';
while (hasFile) {
await new Promise<void>((resolve, reject) => {
// 列举当前目录下的所有文件
this.bucketManager.listPrefix(
this.qiniuConfig.bucket,
{
prefix: dirName,
limit: NETDISK_HANDLE_MAX_ITEM,
marker,
},
(err, respBody, respInfo) => {
if (err) {
reject(err);
return;
}
if (respInfo.statusCode === 200) {
const moveOperations = respBody.items.map((item) => {
const { key } = item;
const destKey = key.replace(dirName, toDirName);
return qiniu.rs.moveOp(
this.qiniuConfig.bucket,
key,
this.qiniuConfig.bucket,
destKey,
op,
);
});
this.bucketManager.batch(
moveOperations,
(err2, respBody2, respInfo2) => {
if (err2) {
reject(err2);
return;
}
if (respInfo2.statusCode === 200) {
if (isEmpty(respBody.marker)) {
hasFile = false;
} else {
marker = respBody.marker;
}
resolve();
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo2.statusCode}, Info: ${respInfo2.statusMessage}`,
),
);
}
},
);
} else {
reject(
new Error(
`Qiniu Error Code: ${respInfo.statusCode}, Info: ${respInfo.statusMessage}`,
),
);
}
},
);
});
}
}
}
}
}
================================================
FILE: src/modules/admin/netdisk/netdisk.module.ts
================================================
import { Module } from '@nestjs/common';
import { qiniuProvider } from '../core/provider/qiniu.provider';
import { SystemModule } from '../system/system.module';
import { NetDiskManageController } from './manager/manage.controller';
import { NetDiskManageService } from './manager/manage.service';
import { NetDiskOverviewController } from './overview/overview.controller';
import { NetDiskOverviewService } from './overview/overview.service';
@Module({
imports: [SystemModule],
controllers: [NetDiskManageController, NetDiskOverviewController],
providers: [NetDiskManageService, NetDiskOverviewService, qiniuProvider()],
})
export class NetdiskModule {}
================================================
FILE: src/modules/admin/netdisk/overview/overview.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
export class SpaceInfo {
@ApiProperty({ description: '当月的X号', type: [Number] })
times: number[];
@ApiProperty({ description: '对应天数的容量, byte单位', type: [Number] })
datas: number[];
}
export class CountInfo {
@ApiProperty({ description: '当月的X号', type: [Number] })
times: number[];
@ApiProperty({ description: '对应天数的文件数量', type: [Number] })
datas: number[];
}
export class FlowInfo {
@ApiProperty({ description: '当月的X号', type: [Number] })
times: number[];
@ApiProperty({ description: '对应天数的耗费流量', type: [Number] })
datas: number[];
}
export class HitInfo {
@ApiProperty({ description: '当月的X号', type: [Number] })
times: number[];
@ApiProperty({ description: '对应天数的Get请求次数', type: [Number] })
datas: number[];
}
export class OverviewSpaceInfo {
@ApiProperty({ description: '当前使用容量' })
spaceSize: number;
@ApiProperty({ description: '当前文件数量' })
fileSize: number;
@ApiProperty({ description: '当天使用流量' })
flowSize: number;
@ApiProperty({ description: '当天请求次数' })
hitSize: number;
@ApiProperty({ description: '流量趋势,从当月1号开始计算', type: FlowInfo })
flowTrend: FlowInfo;
@ApiProperty({ description: '容量趋势,从当月1号开始计算', type: SpaceInfo })
sizeTrend: SpaceInfo;
}
================================================
FILE: src/modules/admin/netdisk/overview/overview.controller.ts
================================================
import {
CacheInterceptor,
CacheKey,
CacheTTL,
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { ADMIN_PREFIX } from '../../admin.constants';
import { PermissionOptional } from '../../core/decorators/permission-optional.decorator';
import { OverviewSpaceInfo } from './overview.class';
import { NetDiskOverviewService } from './overview.service';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('网盘概览模块')
@Controller('overview')
export class NetDiskOverviewController {
constructor(private overviewService: NetDiskOverviewService) {}
@CacheKey('netdisk_overview_desc')
@CacheTTL(3600)
@UseInterceptors(CacheInterceptor)
@ApiOperation({ summary: '获取网盘空间数据统计' })
@ApiOkResponse({ type: OverviewSpaceInfo })
@PermissionOptional()
@Get('desc')
async space(): Promise<OverviewSpaceInfo> {
const date = this.overviewService.getZeroHourAnd1Day(new Date());
const hit = await this.overviewService.getHit(date);
const flow = await this.overviewService.getFlow(date);
const space = await this.overviewService.getSpace(date);
const count = await this.overviewService.getCount(date);
return {
fileSize: count.datas[count.datas.length - 1],
flowSize: flow.datas[flow.datas.length - 1],
hitSize: hit.datas[hit.datas.length - 1],
spaceSize: space.datas[space.datas.length - 1],
flowTrend: flow,
sizeTrend: space,
};
}
}
================================================
FILE: src/modules/admin/netdisk/overview/overview.service.ts
================================================
import { Inject, Injectable } from '@nestjs/common';
import { QINIU_API, QINIU_CONFIG } from '../../admin.constants';
import { IQiniuConfig } from '../../admin.interface';
import * as qiniu from 'qiniu';
import {
getMonth,
getYear,
format,
getDate,
fromUnixTime,
parseISO,
} from 'date-fns';
import { CountInfo, FlowInfo, HitInfo, SpaceInfo } from './overview.class';
import { HttpService } from '@nestjs/axios';
@Injectable()
export class NetDiskOverviewService {
private mac: qiniu.auth.digest.Mac;
private readonly FORMAT = 'yyyyMMddHHmmss';
constructor(
@Inject(QINIU_CONFIG) private qiniuConfig: IQiniuConfig,
private readonly httpService: HttpService,
) {
this.mac = new qiniu.auth.digest.Mac(
this.qiniuConfig.accessKey,
this.qiniuConfig.secretKey,
);
}
/**
* 获取当天零时
*/
getZeroHourToDay(current: Date): Date {
const month = getMonth(current);
const year = getYear(current);
const date = getDate(current);
return new Date(year, month, date, 0);
}
/**
* 获取当月1号零时
*/
getZeroHourAnd1Day(current: Date): Date {
const month = getMonth(current);
const year = getYear(current);
return new Date(year, month, 1, 0);
}
/**
* 该接口可以获取标准存储的当前存储量。可查询当天计量,统计延迟大概 5 分钟。
* https://developer.qiniu.com/kodo/3908/statistic-space
*/
async getSpace(start: Date, end = new Date()): Promise<SpaceInfo> {
const beginDate = format(start, this.FORMAT);
const endDate = format(end, this.FORMAT);
const url = `${QINIU_API}/v6/space?bucket=${this.qiniuConfig.bucket}&g=day&begin=${beginDate}&end=${endDate}`;
const accessToken = qiniu.util.generateAccessTokenV2(
this.mac,
url,
'GET',
'application/x-www-form-urlencoded',
);
const { data } = await this.httpService.axiosRef.get(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `${accessToken}`,
},
});
return {
datas: data.datas,
times: data.times.map((e) => {
return getDate(fromUnixTime(e));
}),
};
}
/**
* 该接口可以获取标准存储的文件数量。可查询当天计量,统计延迟大概 5 分钟。
* https://developer.qiniu.com/kodo/3914/count
*/
async getCount(start: Date, end = new Date()): Promise<CountInfo> {
const beginDate = format(start, this.FORMAT);
const endDate = format(end, this.FORMAT);
const url = `${QINIU_API}/v6/count?bucket=${this.qiniuConfig.bucket}&g=day&begin=${beginDate}&end=${endDate}`;
const accessToken = qiniu.util.generateAccessTokenV2(
this.mac,
url,
'GET',
'application/x-www-form-urlencoded',
);
const { data } = await this.httpService.axiosRef.get(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `${accessToken}`,
},
});
return {
times: data.times.map((e) => {
return getDate(fromUnixTime(e));
}),
datas: data.datas,
};
}
/**
* 外网流出流量统计
* 该接口可以获取外网流出流量、CDN回源流量统计和 GET 请求次数。可查询当天计量,统计延迟大概 5 分钟。
* https://developer.qiniu.com/kodo/3820/blob-io
*/
async getFlow(start: Date, end = new Date()): Promise<FlowInfo> {
const beginDate = format(start, this.FORMAT);
const endDate = format(end, this.FORMAT);
const url = `${QINIU_API}/v6/blob_io?$bucket=${this.qiniuConfig.bucket}&g=day&$ftype=0&begin=${beginDate}&end=${endDate}&$src=origin&select=flow`;
const accessToken = qiniu.util.generateAccessTokenV2(
this.mac,
url,
'GET',
'application/x-www-form-urlencoded',
);
const { data } = await this.httpService.axiosRef.get(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `${accessToken}`,
},
});
const times = [];
const datas = [];
data.forEach((e) => {
times.push(getDate(parseISO(e.time)));
datas.push(e.values.flow);
});
return {
times,
datas,
};
}
/**
* GET 请求次数统计
* 该接口可以获取外网流出流量、CDN回源流量统计和 GET 请求次数。可查询当天计量,统计延迟大概 5 分钟。
* https://developer.qiniu.com/kodo/3820/blob-io
*/
async getHit(start: Date, end = new Date()): Promise<HitInfo> {
const beginDate = format(start, this.FORMAT);
const endDate = format(end, this.FORMAT);
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`;
const accessToken = qiniu.util.generateAccessTokenV2(
this.mac,
url,
'GET',
'application/x-www-form-urlencoded',
);
const { data } = await this.httpService.axiosRef.get(url, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `${accessToken}`,
},
});
const times = [];
const datas = [];
data.forEach((e) => {
times.push(getDate(parseISO(e.time)));
datas.push(e.values.hit);
});
return {
times,
datas,
};
}
}
================================================
FILE: src/modules/admin/system/dept/dept.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import SysDepartment from 'src/entities/admin/sys-department.entity';
export class DeptDetailInfo {
@ApiProperty({ description: '当前查询的部门' })
department?: SysDepartment;
@ApiProperty({ description: '所属父级部门' })
parentDepartment?: SysDepartment;
}
================================================
FILE: src/modules/admin/system/dept/dept.controller.ts
================================================
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { ADMIN_PREFIX } from 'src/modules/admin/admin.constants';
import { ApiException } from 'src/common/exceptions/api.exception';
import SysDepartment from 'src/entities/admin/sys-department.entity';
import { AdminUser } from '../../core/decorators/admin-user.decorator';
import { DeptDetailInfo } from './dept.class';
import {
CreateDeptDto,
DeleteDeptDto,
InfoDeptDto,
MoveDeptDto,
TransferDeptDto,
UpdateDeptDto,
} from './dept.dto';
import { SysDeptService } from './dept.service';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('部门模块')
@Controller('dept')
export class SysDeptController {
constructor(private deptService: SysDeptService) {}
@ApiOperation({ summary: '获取系统部门列表' })
@ApiOkResponse({ type: [SysDepartment] })
@Get('list')
async list(@AdminUser('uid') uid: number): Promise<SysDepartment[]> {
return await this.deptService.getDepts(uid);
}
@ApiOperation({ summary: '创建系统部门' })
@Post('add')
async add(@Body() createDeptDto: CreateDeptDto): Promise<void> {
await this.deptService.add(createDeptDto.name, createDeptDto.parentId);
}
@ApiOperation({ summary: '删除系统部门' })
@Post('delete')
async delete(@Body() deleteDeptDto: DeleteDeptDto): Promise<void> {
// 查询是否有关联用户或者部门,如果含有则无法删除
const count = await this.deptService.countUserByDeptId(
deleteDeptDto.departmentId,
);
if (count > 0) {
throw new ApiException(10009);
}
const count2 = await this.deptService.countRoleByDeptId(
deleteDeptDto.departmentId,
);
if (count2 > 0) {
throw new ApiException(10010);
}
const count3 = await this.deptService.countChildDept(
deleteDeptDto.departmentId,
);
if (count3 > 0) {
throw new ApiException(10015);
}
await this.deptService.delete(deleteDeptDto.departmentId);
}
@ApiOperation({ summary: '查询单个系统部门信息' })
@ApiOkResponse({ type: DeptDetailInfo })
@Get('info')
async info(@Query() infoDeptDto: InfoDeptDto): Promise<DeptDetailInfo> {
return await this.deptService.info(infoDeptDto.departmentId);
}
@ApiOperation({ summary: '更新系统部门' })
@Post('update')
async update(@Body() updateDeptDto: UpdateDeptDto): Promise<void> {
await this.deptService.update(updateDeptDto);
}
@ApiOperation({ summary: '管理员部门转移' })
@Post('transfer')
async transfer(@Body() transferDeptDto: TransferDeptDto): Promise<void> {
await this.deptService.transfer(
transferDeptDto.userIds,
transferDeptDto.departmentId,
);
}
@ApiOperation({ summary: '部门移动排序' })
@Post('move')
async move(@Body() dto: MoveDeptDto): Promise<void> {
await this.deptService.move(dto.depts);
}
}
================================================
FILE: src/modules/admin/system/dept/dept.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayNotEmpty,
IsArray,
IsInt,
IsOptional,
IsString,
Min,
MinLength,
ValidateNested,
} from 'class-validator';
export class CreateDeptDto {
@ApiProperty({ description: '部门名称' })
@IsString()
@MinLength(1)
name: string;
@ApiProperty({ description: '父级部门id' })
@IsInt()
parentId: number;
@ApiProperty({ description: '排序编号', required: false })
@IsOptional()
@IsInt()
@Min(0)
orderNum: number;
}
export class UpdateDeptDto extends CreateDeptDto {
@ApiProperty({ description: '需要更新的部门id' })
@IsInt()
@Min(0)
id: number;
}
export class DeleteDeptDto {
@ApiProperty({ description: '删除的系统部门ID' })
@IsInt()
@Min(0)
departmentId: number;
}
export class InfoDeptDto {
@ApiProperty({ description: '查询的系统部门ID' })
@Type(() => Number)
@IsInt()
@Min(0)
departmentId: number;
}
export class TransferDeptDto {
@ApiProperty({ description: '需要转移的管理员列表编号', type: [Number] })
@IsArray()
@ArrayNotEmpty()
userIds: number[];
@ApiProperty({ description: '需要转移过去的系统部门ID' })
@IsInt()
@Min(0)
departmentId: number;
}
export class MoveDept {
@ApiProperty({ description: '当前部门ID' })
@IsInt()
@Min(0)
id: number;
@ApiProperty({ description: '移动到指定父级部门的ID' })
@IsInt()
@Min(0)
@IsOptional()
parentId: number;
}
export class MoveDeptDto {
@ApiProperty({ description: '部门列表', type: [MoveDept] })
@ValidateNested({ each: true })
@Type(() => MoveDept)
depts: MoveDept[];
}
================================================
FILE: src/modules/admin/system/dept/dept.service.ts
================================================
import { Inject, Injectable } from '@nestjs/common';
import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
import { includes, isEmpty } from 'lodash';
import { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';
import { ApiException } from 'src/common/exceptions/api.exception';
import SysDepartment from 'src/entities/admin/sys-department.entity';
import SysRoleDepartment from 'src/entities/admin/sys-role-department.entity';
import SysUser from 'src/entities/admin/sys-user.entity';
import { EntityManager, In, Repository } from 'typeorm';
import { SysRoleService } from '../role/role.service';
import { DeptDetailInfo } from './dept.class';
import { MoveDept, UpdateDeptDto } from './dept.dto';
@Injectable()
export class SysDeptService {
constructor(
@InjectRepository(SysUser) private userRepositoty: Repository<SysUser>,
@InjectRepository(SysDepartment)
private deptRepositoty: Repository<SysDepartment>,
@InjectRepository(SysRoleDepartment)
private roleDeptRepositoty: Repository<SysUser>,
@InjectEntityManager() private entityManager: EntityManager,
@Inject(ROOT_ROLE_ID) private rootRoleId: number,
private roleService: SysRoleService,
) {}
/**
* 获取所有部门
*/
async list(): Promise<SysDepartment[]> {
return await this.deptRepositoty.find({ order: { orderNum: 'DESC' } });
}
/**
* 根据ID查找部门信息
*/
async info(id: number): Promise<DeptDetailInfo> {
const department = await this.deptRepositoty.findOne({ where: { id } });
if (isEmpty(department)) {
throw new ApiException(10019);
}
let parentDepartment = null;
if (department.parentId) {
parentDepartment = await this.deptRepositoty.findOne({
where: { id: department.parentId },
});
}
return { department, parentDepartment };
}
/**
* 更新部门信息
*/
async update(param: UpdateDeptDto): Promise<void> {
await this.deptRepositoty.update(param.id, {
parentId: param.parentId === -1 ? undefined : param.parentId,
name: param.name,
orderNum: param.orderNum,
});
}
/**
* 转移部门
*/
async transfer(userIds: number[], deptId: number): Promise<void> {
await this.userRepositoty.update(
{ id: In(userIds) },
{ departmentId: deptId },
);
}
/**
* 新增部门
*/
async add(deptName: string, parentDeptId: number): Promise<void> {
await this.deptRepositoty.insert({
name: deptName,
parentId: parentDeptId === -1 ? null : parentDeptId,
});
}
/**
* 移动排序
*/
async move(depts: MoveDept[]): Promise<void> {
await this.entityManager.transaction(async (manager) => {
for (let i = 0; i < depts.length; i++) {
await manager.update(
SysDepartment,
{ id: depts[i].id },
{ parentId: depts[i].parentId },
);
}
});
}
/**
* 根据ID删除部门
*/
async delete(departmentId: number): Promise<void> {
await this.deptRepositoty.delete(departmentId);
}
/**
* 根据部门查询关联的用户数量
*/
async countUserByDeptId(id: number): Promise<number> {
return await this.userRepositoty.count({ where: { departmentId: id } });
}
/**
* 根据部门查询关联的角色数量
*/
async countRoleByDeptId(id: number): Promise<number> {
return await this.roleDeptRepositoty.count({ where: { departmentId: id } });
}
/**
* 查找当前部门下的子部门数量
*/
async countChildDept(id: number): Promise<number> {
return await this.deptRepositoty.count({ where: { parentId: id } });
}
/**
* 根据当前角色id获取部门列表
*/
async getDepts(uid: number): Promise<SysDepartment[]> {
const roleIds = await this.roleService.getRoleIdByUser(uid);
let depts: any = [];
if (includes(roleIds, this.rootRoleId)) {
// root find all
depts = await this.deptRepositoty.find();
} else {
// [ 1, 2, 3 ] role find
depts = await this.deptRepositoty
.createQueryBuilder('dept')
.innerJoinAndSelect(
'sys_role_department',
'role_dept',
'dept.id = role_dept.department_id',
)
.andWhere('role_dept.role_id IN (:...roldIds)', { roldIds: roleIds })
.orderBy('dept.order_num', 'ASC')
.getMany();
}
return depts;
}
}
================================================
FILE: src/modules/admin/system/log/log.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
export class LoginLogInfo {
@ApiProperty({ description: '日志编号' })
id: number;
@ApiProperty({ description: '登录ip' })
ip: string;
@ApiProperty({ description: '系统' })
os: string;
@ApiProperty({ description: '浏览器' })
browser: string;
@ApiProperty({ description: '时间' })
time: string;
@ApiProperty({ description: '登录用户名' })
username: string;
}
export class TaskLogInfo {
@ApiProperty({ description: '日志编号' })
id: number;
@ApiProperty({ description: '任务编号' })
taskId: number;
@ApiProperty({ description: '任务名称' })
name: string;
@ApiProperty({ description: '创建时间' })
createdAt: string;
@ApiProperty({ description: '耗时' })
consumeTime: number;
@ApiProperty({ description: '执行信息' })
detail: string;
@ApiProperty({ description: '任务执行状态' })
status: number;
}
================================================
FILE: src/modules/admin/system/log/log.controller.ts
================================================
import { Controller, Get, Query } from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { PageResult } from 'src/common/class/res.class';
import { PageOptionsDto } from 'src/common/dto/page.dto';
import { ADMIN_PREFIX } from '../../admin.constants';
import { LogDisabled } from '../../core/decorators/log-disabled.decorator';
import { LoginLogInfo, TaskLogInfo } from './log.class';
import { SysLogService } from './log.service';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('日志模块')
@Controller('log')
export class SysLogController {
constructor(private logService: SysLogService) {}
@ApiOperation({ summary: '分页查询登录日志' })
@ApiOkResponse({ type: [LoginLogInfo] })
@LogDisabled()
@Get('login/page')
async loginLogPage(
@Query() dto: PageOptionsDto,
): Promise<PageResult<LoginLogInfo>> {
const list = await this.logService.pageGetLoginLog(dto.page - 1, dto.limit);
const count = await this.logService.countLoginLog();
return {
list,
pagination: {
total: count,
size: dto.limit,
page: dto.page,
},
};
}
@ApiOperation({ summary: '分页查询任务日志' })
@ApiOkResponse({ type: [TaskLogInfo] })
@LogDisabled()
@Get('task/page')
async taskPage(
@Query() dto: PageOptionsDto,
): Promise<PageResult<TaskLogInfo>> {
const list = await this.logService.page(dto.page - 1, dto.limit);
const count = await this.logService.countTaskLog();
return {
list,
pagination: {
total: count,
size: dto.limit,
page: dto.page,
},
};
}
}
================================================
FILE: src/modules/admin/system/log/log.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import SysLoginLog from 'src/entities/admin/sys-login-log.entity';
import SysTaskLog from 'src/entities/admin/sys-task-log.entity';
import { Repository } from 'typeorm';
import { UAParser } from 'ua-parser-js';
import { LoginLogInfo, TaskLogInfo } from './log.class';
@Injectable()
export class SysLogService {
constructor(
@InjectRepository(SysLoginLog)
private loginLogRepository: Repository<SysLoginLog>,
@InjectRepository(SysTaskLog)
private taskLogRepository: Repository<SysTaskLog>,
) {}
/**
* 记录登录日志
*/
async saveLoginLog(uid: number, ip: string, ua: string): Promise<void> {
await this.loginLogRepository.save({
ip,
userId: uid,
ua,
});
}
/**
* 计算登录日志日志总数
*/
async countLoginLog(): Promise<number> {
return await this.loginLogRepository.count();
}
/**
* 分页加载日志信息
*/
async pageGetLoginLog(page: number, count: number): Promise<LoginLogInfo[]> {
// const result = await this.getRepo().admin.sys.LoginLog.find({
// order: {
// id: 'DESC',
// },
// take: count,
// skip: page * count,
// });
const result = await this.loginLogRepository
.createQueryBuilder('login_log')
.innerJoinAndSelect('sys_user', 'user', 'login_log.user_id = user.id')
.orderBy('login_log.created_at', 'DESC')
.offset(page * count)
.limit(count)
.getRawMany();
const parser = new UAParser();
return result.map((e) => {
const u = parser.setUA(e.login_log_ua).getResult();
return {
id: e.login_log_id,
ip: e.login_log_ip,
os: `${u.os.name} ${u.os.version}`,
browser: `${u.browser.name} ${u.browser.version}`,
time: e.login_log_created_at,
username: e.user_username,
};
});
}
/**
* 清空表中的所有数据
*/
async clearLoginLog(): Promise<void> {
await this.loginLogRepository.clear();
}
// ----- task
/**
* 记录任务日志
*/
async recordTaskLog(
tid: number,
status: number,
time?: number,
err?: string,
): Promise<number> {
const result = await this.taskLogRepository.save({
taskId: tid,
status,
detail: err,
});
return result.id;
}
/**
* 计算日志总数
*/
async countTaskLog(): Promise<number> {
return await this.taskLogRepository.count();
}
/**
* 分页加载日志信息
*/
async page(page: number, count: number): Promise<TaskLogInfo[]> {
// const result = await this.getRepo().admin.sys.TaskLog.find({
// order: {
// id: 'DESC',
// },
// take: count,
// skip: page * count,
// });
// return result;
const result = await this.taskLogRepository
.createQueryBuilder('task_log')
.leftJoinAndSelect('sys_task', 'task', 'task_log.task_id = task.id')
.orderBy('task_log.id', 'DESC')
.offset(page * count)
.limit(count)
.getRawMany();
return result.map((e) => {
return {
id: e.task_log_id,
taskId: e.task_id,
name: e.task_name,
createdAt: e.task_log_created_at,
consumeTime: e.task_log_consume_time,
detail: e.task_log_detail,
status: e.task_log_status,
};
});
}
/**
* 清空表中的所有数据
*/
async clearTaskLog(): Promise<void> {
await this.taskLogRepository.clear();
}
}
================================================
FILE: src/modules/admin/system/menu/menu.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import SysMenu from 'src/entities/admin/sys-menu.entity';
export class MenuItemAndParentInfoResult {
@ApiProperty({ description: '菜单' })
menu?: SysMenu;
@ApiProperty({ description: '父级菜单' })
parentMenu?: SysMenu;
}
================================================
FILE: src/modules/admin/system/menu/menu.controller.ts
================================================
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { flattenDeep } from 'lodash';
import {
ADMIN_PREFIX,
FORBIDDEN_OP_MENU_ID_INDEX,
} from 'src/modules/admin/admin.constants';
import { ApiException } from 'src/common/exceptions/api.exception';
import SysMenu from 'src/entities/admin/sys-menu.entity';
import { IAdminUser } from '../../admin.interface';
import { AdminUser } from '../../core/decorators/admin-user.decorator';
import { MenuItemAndParentInfoResult } from './menu.class';
import {
CreateMenuDto,
DeleteMenuDto,
InfoMenuDto,
UpdateMenuDto,
} from './menu.dto';
import { SysMenuService } from './menu.service';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('菜单权限模块')
@Controller('menu')
export class SysMenuController {
constructor(private menuService: SysMenuService) {}
@ApiOperation({ summary: '获取对应权限的菜单列表' })
@ApiOkResponse({ type: [SysMenu] })
@Get('list')
async list(@AdminUser() user: IAdminUser): Promise<SysMenu[]> {
return await this.menuService.getMenus(user.uid);
}
@ApiOperation({ summary: '新增菜单或权限' })
@Post('add')
async add(@Body() dto: CreateMenuDto): Promise<void> {
// check
await this.menuService.check(dto);
if (dto.parentId === -1) {
dto.parentId = null;
}
await this.menuService.save(dto);
if (dto.type === 2) {
// 如果是权限发生更改,则刷新所有在线用户的权限
await this.menuService.refreshOnlineUserPerms();
}
}
@ApiOperation({ summary: '新增菜单或权限' })
@Post('update')
async update(@Body() dto: UpdateMenuDto): Promise<void> {
if (dto.menuId <= FORBIDDEN_OP_MENU_ID_INDEX) {
// 系统内置功能不提供删除
throw new ApiException(10016);
}
// check
await this.menuService.check(dto);
if (dto.parentId === -1) {
dto.parentId = null;
}
const insertData: CreateMenuDto & { id: number } = {
...dto,
id: dto.menuId,
};
await this.menuService.save(insertData);
if (dto.type === 2) {
// 如果是权限发生更改,则刷新所有在线用户的权限
await this.menuService.refreshOnlineUserPerms();
}
}
@ApiOperation({ summary: '删除菜单或权限' })
@Post('delete')
async delete(@Body() dto: DeleteMenuDto): Promise<void> {
// 68为内置init.sql中插入最后的索引编号
if (dto.menuId <= FORBIDDEN_OP_MENU_ID_INDEX) {
// 系统内置功能不提供删除
throw new ApiException(10016);
}
// 如果有子目录,一并删除
const childMenus = await this.menuService.findChildMenus(dto.menuId);
await this.menuService.deleteMenuItem(
flattenDeep([dto.menuId, childMenus]),
);
// 刷新在线用户权限
await this.menuService.refreshOnlineUserPerms();
}
@ApiOperation({ summary: '菜单或权限信息' })
@ApiOkResponse({ type: MenuItemAndParentInfoResult })
@Get('info')
async info(@Query() dto: InfoMenuDto): Promise<MenuItemAndParentInfoResult> {
return await this.menuService.getMenuItemAndParentInfo(dto.menuId);
}
}
================================================
FILE: src/modules/admin/system/menu/menu.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
IsBoolean,
IsIn,
IsInt,
IsOptional,
IsString,
Min,
MinLength,
ValidateIf,
} from 'class-validator';
/**
* 增加菜单
*/
export class CreateMenuDto {
@ApiProperty({ description: '菜单类型' })
@IsIn([0, 1, 2])
type: number;
@ApiProperty({ description: '父级菜单' })
@IsInt()
parentId: number;
@ApiProperty({ description: '菜单或权限名称' })
@IsString()
@MinLength(2)
name: string;
@ApiProperty({ description: '排序' })
@IsInt()
@Min(0)
orderNum: number;
@ApiProperty({ description: '前端路由地址' })
@IsString()
@ValidateIf((o) => o.type !== 2)
router: string;
@ApiProperty({ description: '菜单是否显示', required: false, default: true })
@IsBoolean()
@ValidateIf((o) => o.type !== 2)
readonly isShow: boolean = true;
@ApiProperty({ description: '开启页面缓存', required: false, default: true })
@IsBoolean()
@ValidateIf((o) => o.type === 1)
readonly keepalive: boolean = true;
@ApiProperty({ description: '菜单图标', required: false })
@IsString()
@IsOptional()
@ValidateIf((o) => o.type !== 2)
icon: string;
@ApiProperty({ description: '对应权限' })
@IsString()
@IsOptional()
@ValidateIf((o) => o.type === 2)
perms: string;
@ApiProperty({ description: '菜单路由路径或外链' })
@ValidateIf((o) => o.type !== 2)
@IsString()
@IsOptional()
viewPath: string;
}
export class UpdateMenuDto extends CreateMenuDto {
@ApiProperty({ description: '更新的菜单ID' })
@IsInt()
@Min(0)
menuId: number;
}
/**
* 删除菜单
*/
export class DeleteMenuDto {
@ApiProperty({ description: '删除的菜单ID' })
@IsInt()
@Min(0)
menuId: number;
}
/**
* 查询菜单
*/
export class InfoMenuDto {
@ApiProperty({ description: '查询的菜单ID' })
@IsInt()
@Min(0)
@Type(() => Number)
menuId: number;
}
================================================
FILE: src/modules/admin/system/menu/menu.service.ts
================================================
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { concat, includes, isEmpty, uniq } from 'lodash';
import { ROOT_ROLE_ID } from 'src/modules/admin/admin.constants';
import { ApiException } from 'src/common/exceptions/api.exception';
import SysMenu from 'src/entities/admin/sys-menu.entity';
import { IsNull, Not, Repository } from 'typeorm';
import { SysRoleService } from '../role/role.service';
import { MenuItemAndParentInfoResult } from './menu.class';
import { CreateMenuDto } from './menu.dto';
import { RedisService } from 'src/shared/services/redis.service';
@Injectable()
export class SysMenuService {
constructor(
@InjectRepository(SysMenu) private menuRepository: Repository<SysMenu>,
private redisService: RedisService,
@Inject(ROOT_ROLE_ID) private rootRoleId: number,
private roleService: SysRoleService,
) {}
/**
* 获取所有菜单
*/
async list(): Promise<SysMenu[]> {
return await this.menuRepository.find();
}
/**
* 保存或新增菜单
*/
async save(menu: CreateMenuDto & { id?: number }): Promise<void> {
await this.menuRepository.save(menu);
}
/**
* 根据角色获取所有菜单
*/
async getMenus(uid: number): Promise<SysMenu[]> {
const roleIds = await this.roleService.getRoleIdByUser(uid);
let menus: SysMenu[] = [];
if (includes(roleIds, this.rootRoleId)) {
// root find all
menus = await this.menuRepository.find({
order: { id: 'ASC', orderNum: 'DESC' },
});
} else {
// [ 1, 2, 3 ] role find
menus = await this.menuRepository
.createQueryBuilder('menu')
.innerJoinAndSelect(
'sys_role_menu',
'role_menu',
'menu.id = role_menu.menu_id',
)
.andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds })
.orderBy('menu.order_num', 'DESC')
.orderBy('menu.id', 'ASC')
.getMany();
}
return menus;
}
/**
* 检查菜单创建规则是否符合
*/
async check(dto: CreateMenuDto): Promise<void | never> {
if (dto.type === 2 && dto.parentId === -1) {
// 无法直接创建权限,必须有ParentId
throw new ApiException(10005);
}
if (dto.type === 1 && dto.parentId !== -1) {
const parent = await this.getMenuItemInfo(dto.parentId);
if (isEmpty(parent)) {
throw new ApiException(10014);
}
if (parent && parent.type === 1) {
// 当前新增为菜单但父节点也为菜单时为非法操作
throw new ApiException(10006);
}
}
}
/**
* 查找当前菜单下的子菜单,目录以及菜单
*/
async findChildMenus(mid: number): Promise<any> {
const allMenus: any = [];
const menus = await this.menuRepository.find({ where: { parentId: mid } });
// if (_.isEmpty(menus)) {
// return allMenus;
// }
// const childMenus: any = [];
for (let i = 0; i < menus.length; i++) {
if (menus[i].type !== 2) {
// 子目录下是菜单或目录,继续往下级查找
const c = await this.findChildMenus(menus[i].id);
allMenus.push(c);
}
allMenus.push(menus[i].id);
}
return allMenus;
}
/**
* 获取某个菜单的信息
* @param mid menu id
*/
async getMenuItemInfo(mid: number): Promise<SysMenu> {
const menu = await this.menuRepository.findOne({ where: { id: mid } });
return menu;
}
/**
* 获取某个菜单以及关联的父菜单的信息
*/
async getMenuItemAndParentInfo(
mid: number,
): Promise<MenuItemAndParentInfoResult> {
const menu = await this.menuRepository.findOne({ where: { id: mid } });
let parentMenu: SysMenu | undefined = undefined;
if (menu && menu.parentId) {
parentMenu = await this.menuRepository.findOne({
where: { id: menu.parentId },
});
}
return { menu, parentMenu };
}
/**
* 查找节点路由是否存在
*/
async findRouterExist(router: string): Promise<boolean> {
const menus = await this.menuRepository.findOne({ where: { router } });
return !isEmpty(menus);
}
/**
* 获取当前用户的所有权限
*/
async getPerms(uid: number): Promise<string[]> {
const roleIds = await this.roleService.getRoleIdByUser(uid);
let perms: any[] = [];
let result: any = null;
if (includes(roleIds, this.rootRoleId)) {
// root find all perms
result = await this.menuRepository.find({
where: { perms: Not(IsNull()), type: 2 },
});
} else {
result = await this.menuRepository
.createQueryBuilder('menu')
.innerJoinAndSelect(
'sys_role_menu',
'role_menu',
'menu.id = role_menu.menu_id',
)
.andWhere('role_menu.role_id IN (:...roldIds)', { roldIds: roleIds })
.andWhere('menu.type = 2')
.andWhere('menu.perms IS NOT NULL')
.getMany();
}
if (!isEmpty(result)) {
result.forEach((e) => {
perms = concat(perms, e.perms.split(','));
});
perms = uniq(perms);
}
return perms;
}
/**
* 删除多项菜单
*/
async deleteMenuItem(mids: number[]): Promise<void> {
await this.menuRepository.delete(mids);
}
/**
* 刷新指定用户ID的权限
*/
async refreshPerms(uid: number): Promise<void> {
const perms = await this.getPerms(uid);
const online = await this.redisService.getRedis().get(`admin:token:${uid}`);
if (online) {
// 判断是否在线
await this.redisService
.getRedis()
.set(`admin:perms:${uid}`, JSON.stringify(perms));
}
}
/**
* 刷新所有在线用户的权限
*/
async refreshOnlineUserPerms(): Promise<void> {
const onlineUserIds: string[] = await this.redisService
.getRedis()
.keys('admin:token:*');
if (onlineUserIds && onlineUserIds.length > 0) {
for (let i = 0; i < onlineUserIds.length; i++) {
const uid = onlineUserIds[i].split('admin:token:')[1];
const perms = await this.getPerms(parseInt(uid));
await this.redisService
.getRedis()
.set(`admin:perms:${uid}`, JSON.stringify(perms));
}
}
}
}
================================================
FILE: src/modules/admin/system/online/online.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
export class OnlineUserInfo {
@ApiProperty({ description: '最近的一条登录日志ID' })
id: number;
@ApiProperty({ description: '登录IP' })
ip: string;
@ApiProperty({ description: '用户名' })
username: string;
@ApiProperty({ description: '是否当前' })
isCurrent: boolean;
@ApiProperty({ description: '登陆时间' })
time: string;
@ApiProperty({ description: '系统' })
os: string;
@ApiProperty({ description: '浏览器' })
browser: string;
@ApiProperty({ description: '是否禁用' })
disable: boolean;
}
================================================
FILE: src/modules/admin/system/online/online.controller.ts
================================================
import { Body, Controller, Get, Post } from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { ApiException } from 'src/common/exceptions/api.exception';
import { ADMIN_PREFIX } from '../../admin.constants';
import { IAdminUser } from '../../admin.interface';
import { AdminUser } from '../../core/decorators/admin-user.decorator';
import { LogDisabled } from '../../core/decorators/log-disabled.decorator';
import { OnlineUserInfo } from './online.class';
import { KickDto } from './online.dto';
import { SysOnlineService } from './online.service';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('在线用户模块')
@Controller('online')
export class SysOnlineController {
constructor(private onlineService: SysOnlineService) {}
@ApiOperation({ summary: '查询当前在线用户' })
@ApiOkResponse({ type: [OnlineUserInfo] })
@LogDisabled()
@Get('list')
async list(@AdminUser() user: IAdminUser): Promise<OnlineUserInfo[]> {
return await this.onlineService.listOnlineUser(user.uid);
}
@ApiOperation({ summary: '下线指定在线用户' })
@Post('kick')
async kick(
@Body() dto: KickDto,
@AdminUser() user: IAdminUser,
): Promise<void> {
if (dto.id === user.uid) {
throw new ApiException(10012);
}
await this.onlineService.kickUser(dto.id, user.uid);
}
}
================================================
FILE: src/modules/admin/system/online/online.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { IsInt } from 'class-validator';
export class KickDto {
@ApiProperty({ description: '需要下线的角色ID' })
@IsInt()
id: number;
}
================================================
FILE: src/modules/admin/system/online/online.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectEntityManager } from '@nestjs/typeorm';
import { ApiException } from 'src/common/exceptions/api.exception';
import { AdminWSGateway } from 'src/modules/ws/admin-ws.gateway';
import { EVENT_KICK } from 'src/modules/ws/ws.event';
import { EntityManager } from 'typeorm';
import { UAParser } from 'ua-parser-js';
import { SysUserService } from '../user/user.service';
import { OnlineUserInfo } from './online.class';
import { RemoteSocket } from 'socket.io';
@Injectable()
export class SysOnlineService {
constructor(
@InjectEntityManager() private entityManager: EntityManager,
private userService: SysUserService,
private adminWsGateWay: AdminWSGateway,
private jwtService: JwtService,
) {}
/**
* 罗列在线用户列表
*/
async listOnlineUser(currentUid: number): Promise<OnlineUserInfo[]> {
const onlineSockets = await this.adminWsGateWay.socketServer.fetchSockets();
if (!onlineSockets || onlineSockets.length <= 0) {
return [];
}
const onlineIds = onlineSockets.map((socket) => {
const token = socket.handshake.query?.token as string;
return this.jwtService.verify(token).uid;
});
return await this.findLastLoginInfoList(onlineIds, currentUid);
}
/**
* 下线当前用户
*/
async kickUser(uid: number, currentUid: number): Promise<void> {
const rootUserId = await this.userService.findRootUserId();
const currentUserInfo = await this.userService.getAccountInfo(currentUid);
if (uid === rootUserId) {
throw new ApiException(10013);
}
// reset redis keys
await this.userService.forbidden(uid);
// socket emit
const socket = await this.findSocketIdByUid(uid);
if (socket) {
// socket emit event
this.adminWsGateWay.socketServer
.to(socket.id)
.emit(EVENT_KICK, { operater: currentUserInfo.name });
// close socket
socket.disconnect();
}
}
/**
* 根据uid查找socketid
*/
async findSocketIdByUid(uid: number): Promise<RemoteSocket<any, any>> {
const onlineSockets = await this.adminWsGateWay.socketServer.fetchSockets();
const socket = onlineSockets.find((socket) => {
const token = socket.handshake.query?.token as string;
const tokenUid = this.jwtService.verify(token).uid;
return tokenUid === uid;
});
return socket;
}
/**
* 根据用户id列表查找最近登录信息和用户信息
*/
async findLastLoginInfoList(
ids: number[],
currentUid: number,
): Promise<OnlineUserInfo[]> {
const rootUserId = await this.userService.findRootUserId();
const result = await this.entityManager.query(
`
SELECT sys_login_log.created_at, sys_login_log.ip, sys_login_log.ua, sys_user.id, sys_user.username, sys_user.name
FROM sys_login_log
INNER JOIN sys_user ON sys_login_log.user_id = sys_user.id
WHERE sys_login_log.created_at IN (SELECT MAX(created_at) as createdAt FROM sys_login_log GROUP BY user_id)
AND sys_user.id IN (?)
`,
[ids],
);
if (result) {
const parser = new UAParser();
return result.map((e) => {
const u = parser.setUA(e.ua).getResult();
return {
id: e.id,
ip: e.ip,
username: `${e.name}(${e.username})`,
isCurrent: currentUid === e.id,
time: e.created_at,
os: `${u.os.name} ${u.os.version}`,
browser: `${u.browser.name} ${u.browser.version}`,
disable: currentUid === e.id || e.id === rootUserId,
};
});
}
return [];
}
}
================================================
FILE: src/modules/admin/system/param-config/param-config.controller.ts
================================================
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import {
ApiOkResponse,
ApiOperation,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { PageResult } from 'src/common/class/res.class';
import { PageOptionsDto } from 'src/common/dto/page.dto';
import SysConfig from 'src/entities/admin/sys-config.entity';
import { ADMIN_PREFIX } from '../../admin.constants';
import {
CreateParamConfigDto,
DeleteParamConfigDto,
InfoParamConfigDto,
UpdateParamConfigDto,
} from './param-config.dto';
import { SysParamConfigService } from './param-config.service';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('参数配置模块')
@Controller('param-config')
export class SysParamConfigController {
constructor(private paramConfigService: SysParamConfigService) {}
@ApiOperation({ summary: '分页获取参数配置列表' })
@ApiOkResponse({ type: [SysConfig] })
@Get('page')
async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysConfig>> {
const list = await this.paramConfigService.getConfigListByPage(
dto.page - 1,
dto.limit,
);
const count = await this.paramConfigService.countConfigList();
return {
pagination: {
total: count,
size: dto.limit,
page: dto.page,
},
list,
};
}
@ApiOperation({ summary: '新增参数配置' })
@Post('add')
async add(@Body() dto: CreateParamConfigDto): Promise<void> {
await this.paramConfigService.isExistKey(dto.key);
await this.paramConfigService.add(dto);
}
@ApiOperation({ summary: '查询单个参数配置信息' })
@ApiOkResponse({ type: SysConfig })
@Get('info')
async info(@Query() dto: InfoParamConfigDto): Promise<SysConfig> {
return this.paramConfigService.findOne(dto.id);
}
@ApiOperation({ summary: '更新单个参数配置' })
@Post('update')
async update(@Body() dto: UpdateParamConfigDto): Promise<void> {
await this.paramConfigService.update(dto);
}
@ApiOperation({ summary: '删除指定的参数配置' })
@Post('delete')
async delete(@Body() dto: DeleteParamConfigDto): Promise<void> {
await this.paramConfigService.delete(dto.ids);
}
}
================================================
FILE: src/modules/admin/system/param-config/param-config.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayNotEmpty,
IsArray,
IsInt,
IsOptional,
IsString,
Min,
MinLength,
} from 'class-validator';
export class CreateParamConfigDto {
@ApiProperty({ description: '参数名称' })
@IsString()
name: string;
@ApiProperty({ description: '参数键名' })
@IsString()
@MinLength(3)
key: string;
@ApiProperty({ description: '参数值' })
@IsString()
value: string;
@ApiProperty({ required: false, description: '备注' })
@IsString()
@IsOptional()
remark: string;
}
export class UpdateParamConfigDto {
@ApiProperty({ description: '配置编号' })
@IsInt()
@Min(1)
id: number;
@ApiProperty({ description: '参数名称' })
@IsString()
name: string;
@ApiProperty({ description: '参数值' })
@IsString()
value: string;
@ApiProperty({ required: false, description: '备注' })
@IsString()
@IsOptional()
remark: string;
}
export class DeleteParamConfigDto {
@ApiProperty({ description: '需要删除的配置id列表', type: [Number] })
@IsArray()
@ArrayNotEmpty()
ids: number[];
}
export class InfoParamConfigDto {
@ApiProperty({ description: '需要查询的配置编号' })
@IsInt()
@Min(0)
@Type(() => Number)
id: number;
}
================================================
FILE: src/modules/admin/system/param-config/param-config.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ApiException } from 'src/common/exceptions/api.exception';
import SysConfig from 'src/entities/admin/sys-config.entity';
import { Repository } from 'typeorm';
import { CreateParamConfigDto, UpdateParamConfigDto } from './param-config.dto';
@Injectable()
export class SysParamConfigService {
constructor(
@InjectRepository(SysConfig)
private configRepository: Repository<SysConfig>,
) {}
/**
* 罗列所有配置
*/
async getConfigListByPage(page: number, count: number): Promise<SysConfig[]> {
return this.configRepository.find({
order: {
id: 'ASC',
},
take: count,
skip: page * count,
});
}
/**
* 获取参数总数
*/
async countConfigList(): Promise<number> {
return this.configRepository.count();
}
/**
* 新增
*/
async add(dto: CreateParamConfigDto): Promise<void> {
await this.configRepository.insert(dto);
}
/**
* 更新
*/
async update(dto: UpdateParamConfigDto): Promise<void> {
await this.configRepository.update(
{ id: dto.id },
{ name: dto.name, value: dto.value, remark: dto.remark },
);
}
/**
* 删除
*/
async delete(ids: number[]): Promise<void> {
await this.configRepository.delete(ids);
}
/**
* 查询单个
*/
async findOne(id: number): Promise<SysConfig> {
return await this.configRepository.findOne({ where: { id } });
}
async isExistKey(key: string): Promise<void | never> {
const result = await this.configRepository.findOne({ where: { key } });
if (result) {
throw new ApiException(10021);
}
}
async findValueByKey(key: string): Promise<string | null> {
const result = await this.configRepository.findOne({
where: { key },
select: ['value'],
});
if (result) {
return result.value;
}
return null;
}
}
================================================
FILE: src/modules/admin/system/role/role.class.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import SysRoleDepartment from 'src/entities/admin/sys-role-department.entity';
import SysRoleMenu from 'src/entities/admin/sys-role-menu.entity';
import SysRole from 'src/entities/admin/sys-role.entity';
export class RoleInfo {
@ApiProperty({
type: SysRole,
})
roleInfo: SysRole;
@ApiProperty({
type: [SysRoleMenu],
})
menus: SysRoleMenu[];
@ApiProperty({
type: [SysRoleDepartment],
})
depts: SysRoleDepartment[];
}
export class CreatedRoleId {
roleId: number;
}
================================================
FILE: src/modules/admin/system/role/role.controller.ts
================================================
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
import {
ApiOperation,
ApiOkResponse,
ApiSecurity,
ApiTags,
} from '@nestjs/swagger';
import { ADMIN_PREFIX } from 'src/modules/admin/admin.constants';
import { PageOptionsDto } from 'src/common/dto/page.dto';
import { PageResult } from 'src/common/class/res.class';
import SysRole from 'src/entities/admin/sys-role.entity';
import { SysRoleService } from './role.service';
import {
CreateRoleDto,
DeleteRoleDto,
InfoRoleDto,
UpdateRoleDto,
} from './role.dto';
import { ApiException } from 'src/common/exceptions/api.exception';
import { AdminUser } from '../../core/decorators/admin-user.decorator';
import { IAdminUser } from '../../admin.interface';
import { RoleInfo } from './role.class';
import { SysMenuService } from '../menu/menu.service';
@ApiSecurity(ADMIN_PREFIX)
@ApiTags('角色模块')
@Controller('role')
export class SysRoleController {
constructor(
private roleService: SysRoleService,
private menuService: SysMenuService,
) {}
@ApiOperation({ summary: '获取角色列表' })
@ApiOkResponse({ type: [SysRole] })
@Get('list')
async list(): Promise<SysRole[]> {
return await this.roleService.list();
}
@ApiOperation({ summary: '分页查询角色信息' })
@ApiOkResponse({ type: [SysRole] })
@Get('page')
async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysRole>> {
const list = await this.roleService.page(dto.page - 1, dto.limit);
const count = await this.roleService.count();
return {
list,
pagination: {
size: dto.limit,
page: dto.page,
total: count,
},
};
}
@ApiOperation({ summary: '删除角色' })
@Post('delete')
async delete(@Body() dto: DeleteRoleDto): Promise<void> {
const count = await this.roleService.countUserIdByRole(dto.roleIds);
if (count > 0) {
throw new ApiException(10008);
}
await this.roleService.delete(dto.roleIds);
await this.menuService.refreshOnlineUserPerms();
}
@ApiOperation({ summary: '新增角色' })
@Post('add')
async add(
@Body() dto: CreateRoleDto,
@AdminUser() user: IAdminUser,
): Promise<void> {
await this.roleService.add(dto, user.uid);
}
@ApiOperation({ summary: '更新角色' })
@Post('update')
async update(@Body() dto: UpdateRoleDto): Promise<void> {
await this.roleService.update(dto);
await this.menuService.refreshOnlineUserPerms();
}
@ApiOperation({ summary: '获取角色信息' })
@ApiOkResponse({ type: RoleInfo })
@Get('info')
async info(@Query() dto: InfoRoleDto): Promise<RoleInfo> {
return await this.roleService.info(dto.roleId);
}
}
================================================
FILE: src/modules/admin/system/role/role.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayNotEmpty,
IsArray,
IsInt,
IsOptional,
IsString,
Matches,
Min,
MinLength,
} from 'class-validator';
export class DeleteRoleDto {
@ApiProperty({
description: '需要删除的角色ID列表',
type: [Number],
})
gitextract_j_pyb6i1/ ├── .dockerignore ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── build-rc.yml │ └── build-stable.yml ├── .gitignore ├── .prettierrc ├── Dockerfile ├── LICENSE ├── README.md ├── docs/ │ ├── sample/ │ │ ├── config.development.ts │ │ └── docker-compose.yml │ ├── 权限管理数据库设计文档.md │ └── 通用协作规范.md ├── nest-cli.json ├── package.json ├── sql/ │ ├── init.sql │ ├── upgrade_20210508.sql │ ├── upgrade_20210914.sql.migrate │ └── upgrade_20210927.sql ├── src/ │ ├── app.module.ts │ ├── common/ │ │ ├── class/ │ │ │ └── res.class.ts │ │ ├── contants/ │ │ │ ├── decorator.contants.ts │ │ │ ├── error-code.contants.ts │ │ │ └── param-config.contants.ts │ │ ├── decorators/ │ │ │ └── keep.decorator.ts │ │ ├── dto/ │ │ │ └── page.dto.ts │ │ ├── exceptions/ │ │ │ ├── api.exception.ts │ │ │ └── socket.exception.ts │ │ ├── filters/ │ │ │ └── api-exception.filter.ts │ │ └── interceptors/ │ │ └── api-transform.interceptor.ts │ ├── config/ │ │ ├── config.default.ts │ │ ├── config.production.ts │ │ ├── configuration.ts │ │ ├── defineConfig.ts │ │ └── env.ts │ ├── entities/ │ │ ├── admin/ │ │ │ ├── sys-config.entity.ts │ │ │ ├── sys-department.entity.ts │ │ │ ├── sys-login-log.entity.ts │ │ │ ├── sys-menu.entity.ts │ │ │ ├── sys-role-department.entity.ts │ │ │ ├── sys-role-menu.entity.ts │ │ │ ├── sys-role.entity.ts │ │ │ ├── sys-task-log.entity.ts │ │ │ ├── sys-task.entity.ts │ │ │ ├── sys-user-role.entity.ts │ │ │ └── sys-user.entity.ts │ │ └── base.entity.ts │ ├── main.ts │ ├── mission/ │ │ ├── README.md │ │ ├── jobs/ │ │ │ ├── http-request.job.ts │ │ │ └── sys-log-clear.job.ts │ │ ├── mission.decorator.ts │ │ └── mission.module.ts │ ├── modules/ │ │ ├── admin/ │ │ │ ├── account/ │ │ │ │ ├── account.controller.ts │ │ │ │ ├── account.dto.ts │ │ │ │ └── account.module.ts │ │ │ ├── admin.constants.ts │ │ │ ├── admin.interface.ts │ │ │ ├── admin.module.ts │ │ │ ├── core/ │ │ │ │ ├── decorators/ │ │ │ │ │ ├── admin-user.decorator.ts │ │ │ │ │ ├── authorize.decorator.ts │ │ │ │ │ ├── log-disabled.decorator.ts │ │ │ │ │ └── permission-optional.decorator.ts │ │ │ │ ├── guards/ │ │ │ │ │ └── auth.guard.ts │ │ │ │ └── provider/ │ │ │ │ ├── qiniu.provider.ts │ │ │ │ └── root-role-id.provider.ts │ │ │ ├── login/ │ │ │ │ ├── login.class.ts │ │ │ │ ├── login.controller.ts │ │ │ │ ├── login.dto.ts │ │ │ │ ├── login.module.ts │ │ │ │ └── login.service.ts │ │ │ ├── netdisk/ │ │ │ │ ├── manager/ │ │ │ │ │ ├── manage.class.ts │ │ │ │ │ ├── manage.controller.ts │ │ │ │ │ ├── manage.dto.ts │ │ │ │ │ └── manage.service.ts │ │ │ │ ├── netdisk.module.ts │ │ │ │ └── overview/ │ │ │ │ ├── overview.class.ts │ │ │ │ ├── overview.controller.ts │ │ │ │ └── overview.service.ts │ │ │ └── system/ │ │ │ ├── dept/ │ │ │ │ ├── dept.class.ts │ │ │ │ ├── dept.controller.ts │ │ │ │ ├── dept.dto.ts │ │ │ │ └── dept.service.ts │ │ │ ├── log/ │ │ │ │ ├── log.class.ts │ │ │ │ ├── log.controller.ts │ │ │ │ └── log.service.ts │ │ │ ├── menu/ │ │ │ │ ├── menu.class.ts │ │ │ │ ├── menu.controller.ts │ │ │ │ ├── menu.dto.ts │ │ │ │ └── menu.service.ts │ │ │ ├── online/ │ │ │ │ ├── online.class.ts │ │ │ │ ├── online.controller.ts │ │ │ │ ├── online.dto.ts │ │ │ │ └── online.service.ts │ │ │ ├── param-config/ │ │ │ │ ├── param-config.controller.ts │ │ │ │ ├── param-config.dto.ts │ │ │ │ └── param-config.service.ts │ │ │ ├── role/ │ │ │ │ ├── role.class.ts │ │ │ │ ├── role.controller.ts │ │ │ │ ├── role.dto.ts │ │ │ │ └── role.service.ts │ │ │ ├── serve/ │ │ │ │ ├── serve.class.ts │ │ │ │ ├── serve.controller.ts │ │ │ │ └── serve.service.ts │ │ │ ├── system.module.ts │ │ │ ├── task/ │ │ │ │ ├── task.controller.ts │ │ │ │ ├── task.dto.ts │ │ │ │ ├── task.processor.ts │ │ │ │ └── task.service.ts │ │ │ └── user/ │ │ │ ├── user.class.ts │ │ │ ├── user.controller.ts │ │ │ ├── user.dto.ts │ │ │ └── user.service.ts │ │ └── ws/ │ │ ├── admin-ws.gateway.ts │ │ ├── admin-ws.guard.ts │ │ ├── auth.service.ts │ │ ├── ws.event.ts │ │ └── ws.module.ts │ ├── polyfill.ts │ ├── setup-swagger.ts │ └── shared/ │ ├── logger/ │ │ ├── logger.constants.ts │ │ ├── logger.interface.ts │ │ ├── logger.module.ts │ │ ├── logger.service.ts │ │ ├── typeorm-logger.service.ts │ │ └── utils/ │ │ ├── app-root-path.util.ts │ │ └── home-dir.ts │ ├── redis/ │ │ ├── redis.constants.ts │ │ ├── redis.interface.ts │ │ └── redis.module.ts │ ├── services/ │ │ ├── redis.service.ts │ │ └── util.service.ts │ └── shared.module.ts ├── test/ │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json └── tsconfig.json
SYMBOL INDEX (487 symbols across 103 files)
FILE: sql/init.sql
type `sys_department` (line 12) | CREATE TABLE `sys_department` (
type `sys_login_log` (line 34) | CREATE TABLE `sys_login_log` (
type `sys_menu` (line 49) | CREATE TABLE `sys_menu` (
type `sys_req_log` (line 121) | CREATE TABLE `sys_req_log` (
type `sys_role` (line 139) | CREATE TABLE `sys_role` (
type `sys_role_department` (line 163) | CREATE TABLE `sys_role_department` (
type `sys_role_menu` (line 176) | CREATE TABLE `sys_role_menu` (
type `sys_task` (line 189) | CREATE TABLE `sys_task` (
type `sys_task_log` (line 222) | CREATE TABLE `sys_task_log` (
type `sys_user` (line 237) | CREATE TABLE `sys_user` (
type `sys_user_role` (line 267) | CREATE TABLE `sys_user_role` (
FILE: sql/upgrade_20210927.sql
type `sys_config` (line 4) | CREATE TABLE `sys_config` (
FILE: src/app.module.ts
class AppModule (line 86) | class AppModule {}
FILE: src/common/class/res.class.ts
class ResOp (line 1) | class ResOp {
method constructor (line 6) | constructor(code: number, data?: any, message = 'success') {
method success (line 12) | static success(data?: any) {
class Pagination (line 17) | class Pagination {
class PageResult (line 23) | class PageResult<T> {
FILE: src/common/contants/decorator.contants.ts
constant TRANSFORM_KEEP_KEY_METADATA (line 2) | const TRANSFORM_KEEP_KEY_METADATA = 'common:transform_keep';
constant MISSION_KEY_METADATA (line 5) | const MISSION_KEY_METADATA = 'common:mission';
FILE: src/common/contants/param-config.contants.ts
constant SYS_USER_INITPASSWORD (line 1) | const SYS_USER_INITPASSWORD = 'sys_user_initPassword';
FILE: src/common/dto/page.dto.ts
class PageOptionsDto (line 5) | class PageOptionsDto {
FILE: src/common/exceptions/api.exception.ts
class ApiException (line 7) | class ApiException extends HttpException {
method constructor (line 13) | constructor(errorCode: number) {
method getErrorCode (line 18) | getErrorCode(): number {
FILE: src/common/exceptions/socket.exception.ts
class SocketException (line 4) | class SocketException extends WsException {
method constructor (line 7) | constructor(errorCode: number) {
method getErrorCode (line 12) | getErrorCode(): number {
FILE: src/common/filters/api-exception.filter.ts
class ApiExceptionFilter (line 18) | class ApiExceptionFilter implements ExceptionFilter {
method constructor (line 19) | constructor(private logger: LoggerService) {}
method catch (line 21) | catch(exception: unknown, host: ArgumentsHost) {
FILE: src/common/interceptors/api-transform.interceptor.ts
class ApiTransformInterceptor (line 12) | class ApiTransformInterceptor implements NestInterceptor {
method constructor (line 13) | constructor(private readonly reflector: Reflector) {}
method intercept (line 14) | intercept(
FILE: src/config/defineConfig.ts
function defineConfig (line 8) | function defineConfig(config: IConfig): IConfig {
type IConfig (line 15) | interface IConfig {
type JwtConfigOptions (line 48) | interface JwtConfigOptions {
type QiniuConfigOptions (line 52) | interface QiniuConfigOptions {
type RedisConfigOptions (line 61) | interface RedisConfigOptions {
type DataBaseConfigOptions (line 68) | interface DataBaseConfigOptions {
type SwaggerConfigOptions (line 79) | interface SwaggerConfigOptions {
FILE: src/config/env.ts
function isDev (line 5) | function isDev(): boolean {
FILE: src/entities/admin/sys-config.entity.ts
class SysConfig (line 6) | class SysConfig extends BaseEntity {
FILE: src/entities/admin/sys-department.entity.ts
class SysDepartment (line 6) | class SysDepartment extends BaseEntity {
FILE: src/entities/admin/sys-login-log.entity.ts
class SysLoginLog (line 6) | class SysLoginLog extends BaseEntity {
FILE: src/entities/admin/sys-menu.entity.ts
class SysMenu (line 6) | class SysMenu extends BaseEntity {
FILE: src/entities/admin/sys-role-department.entity.ts
class SysRoleDepartment (line 6) | class SysRoleDepartment extends BaseEntity {
FILE: src/entities/admin/sys-role-menu.entity.ts
class SysRoleMenu (line 6) | class SysRoleMenu extends BaseEntity {
FILE: src/entities/admin/sys-role.entity.ts
class SysRole (line 6) | class SysRole extends BaseEntity {
FILE: src/entities/admin/sys-task-log.entity.ts
class SysTaskLog (line 6) | class SysTaskLog extends BaseEntity {
FILE: src/entities/admin/sys-task.entity.ts
class SysTask (line 6) | class SysTask extends BaseEntity {
FILE: src/entities/admin/sys-user-role.entity.ts
class SysUserRole (line 6) | class SysUserRole extends BaseEntity {
FILE: src/entities/admin/sys-user.entity.ts
class SysUser (line 6) | class SysUser extends BaseEntity {
FILE: src/main.ts
function bootstrap (line 20) | async function bootstrap() {
FILE: src/mission/jobs/http-request.job.ts
class HttpRequestJob (line 11) | class HttpRequestJob {
method constructor (line 12) | constructor(
method handle (line 21) | async handle(config: unknown): Promise<void> {
FILE: src/mission/jobs/sys-log-clear.job.ts
class SysLogClearJob (line 10) | class SysLogClearJob {
method constructor (line 11) | constructor(private sysLogService: SysLogService) {}
method clearLoginLog (line 13) | async clearLoginLog(): Promise<void> {
method clearTaskLog (line 17) | async clearTaskLog(): Promise<void> {
FILE: src/mission/mission.module.ts
function createAliasProviders (line 16) | function createAliasProviders(): ExistingProvider[] {
class MissionModule (line 31) | class MissionModule {
method forRoot (line 32) | static forRoot(): DynamicModule {
FILE: src/modules/admin/account/account.controller.ts
class AccountController (line 22) | class AccountController {
method constructor (line 23) | constructor(
method info (line 32) | async info(@AdminUser() user: IAdminUser): Promise<AccountInfo> {
method update (line 39) | async update(
method password (line 49) | async password(
method logout (line 59) | async logout(@AdminUser() user: IAdminUser): Promise<void> {
method permmenu (line 67) | async permmenu(@AdminUser() user: IAdminUser): Promise<PermMenuInfo> {
FILE: src/modules/admin/account/account.dto.ts
class UpdatePersonInfoDto (line 5) | class UpdatePersonInfoDto {
FILE: src/modules/admin/account/account.module.ts
class AccountModule (line 10) | class AccountModule {}
FILE: src/modules/admin/admin.constants.ts
constant ADMIN_USER (line 1) | const ADMIN_USER = 'adminUser';
constant AUTHORIZE_KEY_METADATA (line 2) | const AUTHORIZE_KEY_METADATA = 'admin_module:authorize';
constant PERMISSION_OPTIONAL_KEY_METADATA (line 3) | const PERMISSION_OPTIONAL_KEY_METADATA =
constant LOG_DISABLED_KEY_METADATA (line 5) | const LOG_DISABLED_KEY_METADATA = 'admin_module:log_disabled';
constant ROOT_ROLE_ID (line 7) | const ROOT_ROLE_ID = 'admin_module:root_role_id';
constant QINIU_CONFIG (line 8) | const QINIU_CONFIG = 'admin_module:qiniu_config';
constant SYS_TASK_QUEUE_NAME (line 10) | const SYS_TASK_QUEUE_NAME = 'admin_module:sys-task';
constant SYS_TASK_QUEUE_PREFIX (line 11) | const SYS_TASK_QUEUE_PREFIX = 'admin:sys:task';
constant FORBIDDEN_OP_MENU_ID_INDEX (line 13) | const FORBIDDEN_OP_MENU_ID_INDEX = 91;
constant ADMIN_PREFIX (line 15) | const ADMIN_PREFIX = 'admin';
constant QINIU_API (line 16) | const QINIU_API = 'http://api.qiniu.com';
constant NETDISK_TASK_PREFIX (line 18) | const NETDISK_TASK_PREFIX = 'admin:netdisk:';
constant NETDISK_DELIMITER (line 21) | const NETDISK_DELIMITER = '/';
constant NETDISK_LIMIT (line 22) | const NETDISK_LIMIT = 100;
constant NETDISK_HANDLE_MAX_ITEM (line 23) | const NETDISK_HANDLE_MAX_ITEM = 1000;
constant NETDISK_COPY_SUFFIX (line 24) | const NETDISK_COPY_SUFFIX = '的副本';
FILE: src/modules/admin/admin.interface.ts
type IAdminUser (line 3) | interface IAdminUser {
type QINIU_ACCESS_CONTROL (line 8) | type QINIU_ACCESS_CONTROL = 'private' | 'public';
type IQiniuConfig (line 10) | interface IQiniuConfig {
FILE: src/modules/admin/admin.module.ts
class AdminModule (line 45) | class AdminModule {}
FILE: src/modules/admin/core/guards/auth.guard.ts
class AuthGuard (line 19) | class AuthGuard implements CanActivate {
method constructor (line 20) | constructor(
method canActivate (line 26) | async canActivate(context: ExecutionContext): Promise<boolean> {
FILE: src/modules/admin/core/provider/qiniu.provider.ts
function qiniuProvider (line 9) | function qiniuProvider(): FactoryProvider {
FILE: src/modules/admin/core/provider/root-role-id.provider.ts
function rootRoleIdProvider (line 8) | function rootRoleIdProvider(): FactoryProvider {
FILE: src/modules/admin/login/login.class.ts
class ImageCaptcha (line 4) | class ImageCaptcha {
class LoginToken (line 16) | class LoginToken {
class PermMenuInfo (line 21) | class PermMenuInfo {
FILE: src/modules/admin/login/login.controller.ts
class LoginController (line 21) | class LoginController {
method constructor (line 22) | constructor(private loginService: LoginService, private utils: UtilSer...
method captchaByImg (line 30) | async captchaByImg(@Query() dto: ImageCaptchaDto): Promise<ImageCaptch...
method login (line 41) | async login(
FILE: src/modules/admin/login/login.dto.ts
class ImageCaptchaDto (line 11) | class ImageCaptchaDto {
class LoginInfoDto (line 33) | class LoginInfoDto {
FILE: src/modules/admin/login/login.module.ts
class LoginModule (line 12) | class LoginModule {}
FILE: src/modules/admin/login/login.service.ts
class LoginService (line 15) | class LoginService {
method constructor (line 16) | constructor(
method createImageCaptcha (line 30) | async createImageCaptcha(captcha: ImageCaptchaDto): Promise<ImageCaptc...
method checkImgCaptcha (line 55) | async checkImgCaptcha(id: string, code: string): Promise<void> {
method getLoginSign (line 70) | async getLoginSign(
method clearLoginStatus (line 111) | async clearLoginStatus(uid: number): Promise<void> {
method getPermMenu (line 118) | async getPermMenu(uid: number): Promise<PermMenuInfo> {
method getRedisPasswordVersionById (line 124) | async getRedisPasswordVersionById(id: number): Promise<string> {
method getRedisTokenById (line 128) | async getRedisTokenById(id: number): Promise<string> {
method getRedisPermsById (line 132) | async getRedisPermsById(id: number): Promise<string> {
FILE: src/modules/admin/netdisk/manager/manage.class.ts
type FileType (line 3) | type FileType = 'file' | 'dir';
class SFileInfo (line 5) | class SFileInfo {
class SFileList (line 28) | class SFileList {
class UploadToken (line 36) | class UploadToken {
class SFileInfoDetail (line 41) | class SFileInfoDetail {
FILE: src/modules/admin/netdisk/manager/manage.controller.ts
class NetDiskManageController (line 27) | class NetDiskManageController {
method constructor (line 28) | constructor(private manageService: NetDiskManageService) {}
method list (line 33) | async list(@Query() dto: GetFileListDto): Promise<SFileList> {
method mkdir (line 39) | async mkdir(@Body() dto: MKDirDto): Promise<void> {
method token (line 52) | async token(@AdminUser() user: IAdminUser): Promise<UploadToken> {
method info (line 61) | async info(@Body() dto: FileInfoDto): Promise<SFileInfoDetail> {
method mark (line 67) | async mark(@Body() dto: MarkFileDto): Promise<void> {
method download (line 76) | async download(@Body() dto: FileInfoDto): Promise<string> {
method rename (line 82) | async rename(@Body() dto: RenameDto): Promise<void> {
method delete (line 98) | async delete(@Body() dto: DeleteDto): Promise<void> {
method cut (line 104) | async cut(@Body() dto: FileOpDto): Promise<void> {
method copy (line 117) | async copy(@Body() dto: FileOpDto): Promise<void> {
FILE: src/modules/admin/netdisk/manager/manage.dto.ts
class IsLegalNameExpression (line 20) | class IsLegalNameExpression implements ValidatorConstraintInterface {
method validate (line 22) | validate(value: string, args: ValidationArguments) {
method defaultMessage (line 37) | defaultMessage(_args: ValidationArguments) {
class FileOpItem (line 43) | class FileOpItem {
class GetFileListDto (line 56) | class GetFileListDto {
class MKDirDto (line 73) | class MKDirDto {
class RenameDto (line 85) | class RenameDto {
class FileInfoDto (line 108) | class FileInfoDto {
class DeleteDto (line 120) | class DeleteDto {
class MarkFileDto (line 132) | class MarkFileDto {
class FileOpDto (line 148) | class FileOpDto {
FILE: src/modules/admin/netdisk/manager/manage.service.ts
class NetDiskManageService (line 21) | class NetDiskManageService {
method constructor (line 26) | constructor(
method getFileList (line 48) | async getFileList(prefix = '', marker = '', skey = ''): Promise<SFileL...
method getFileInfo (line 155) | async getFileInfo(name: string, path: string): Promise<SFileInfoDetail> {
method changeFileHeaders (line 205) | async changeFileHeaders(
method createDir (line 238) | async createDir(dirName: string): Promise<void> {
method checkFileExist (line 271) | async checkFileExist(filePath: string): Promise<boolean> {
method createUploadToken (line 306) | createUploadToken(endUser: string): string {
method renameFile (line 321) | async renameFile(dir: string, name: string, toName: string): Promise<v...
method moveFile (line 356) | async moveFile(dir: string, toDir: string, name: string): Promise<void> {
method copyFile (line 391) | async copyFile(dir: string, toDir: string, name: string): Promise<void> {
method renameDir (line 429) | async renameDir(path: string, name: string, toName: string): Promise<v...
method getDownloadLink (line 506) | getDownloadLink(key: string): string {
method deleteFile (line 524) | async deleteFile(dir: string, name: string): Promise<void> {
method deleteMultiFileOrDir (line 553) | async deleteMultiFileOrDir(
method copyMultiFileOrDir (line 653) | async copyMultiFileOrDir(
method moveMultiFileOrDir (line 775) | async moveMultiFileOrDir(
FILE: src/modules/admin/netdisk/netdisk.module.ts
class NetdiskModule (line 14) | class NetdiskModule {}
FILE: src/modules/admin/netdisk/overview/overview.class.ts
class SpaceInfo (line 3) | class SpaceInfo {
class CountInfo (line 11) | class CountInfo {
class FlowInfo (line 19) | class FlowInfo {
class HitInfo (line 27) | class HitInfo {
class OverviewSpaceInfo (line 35) | class OverviewSpaceInfo {
FILE: src/modules/admin/netdisk/overview/overview.controller.ts
class NetDiskOverviewController (line 23) | class NetDiskOverviewController {
method constructor (line 24) | constructor(private overviewService: NetDiskOverviewService) {}
method space (line 33) | async space(): Promise<OverviewSpaceInfo> {
FILE: src/modules/admin/netdisk/overview/overview.service.ts
class NetDiskOverviewService (line 17) | class NetDiskOverviewService {
method constructor (line 21) | constructor(
method getZeroHourToDay (line 34) | getZeroHourToDay(current: Date): Date {
method getZeroHourAnd1Day (line 44) | getZeroHourAnd1Day(current: Date): Date {
method getSpace (line 54) | async getSpace(start: Date, end = new Date()): Promise<SpaceInfo> {
method getCount (line 82) | async getCount(start: Date, end = new Date()): Promise<CountInfo> {
method getFlow (line 111) | async getFlow(start: Date, end = new Date()): Promise<FlowInfo> {
method getHit (line 144) | async getHit(start: Date, end = new Date()): Promise<HitInfo> {
FILE: src/modules/admin/system/dept/dept.class.ts
class DeptDetailInfo (line 4) | class DeptDetailInfo {
FILE: src/modules/admin/system/dept/dept.controller.ts
class SysDeptController (line 26) | class SysDeptController {
method constructor (line 27) | constructor(private deptService: SysDeptService) {}
method list (line 32) | async list(@AdminUser('uid') uid: number): Promise<SysDepartment[]> {
method add (line 38) | async add(@Body() createDeptDto: CreateDeptDto): Promise<void> {
method delete (line 44) | async delete(@Body() deleteDeptDto: DeleteDeptDto): Promise<void> {
method info (line 70) | async info(@Query() infoDeptDto: InfoDeptDto): Promise<DeptDetailInfo> {
method update (line 76) | async update(@Body() updateDeptDto: UpdateDeptDto): Promise<void> {
method transfer (line 82) | async transfer(@Body() transferDeptDto: TransferDeptDto): Promise<void> {
method move (line 91) | async move(@Body() dto: MoveDeptDto): Promise<void> {
FILE: src/modules/admin/system/dept/dept.dto.ts
class CreateDeptDto (line 14) | class CreateDeptDto {
class UpdateDeptDto (line 31) | class UpdateDeptDto extends CreateDeptDto {
class DeleteDeptDto (line 38) | class DeleteDeptDto {
class InfoDeptDto (line 45) | class InfoDeptDto {
class TransferDeptDto (line 53) | class TransferDeptDto {
class MoveDept (line 65) | class MoveDept {
class MoveDeptDto (line 78) | class MoveDeptDto {
FILE: src/modules/admin/system/dept/dept.service.ts
class SysDeptService (line 15) | class SysDeptService {
method constructor (line 16) | constructor(
method list (line 30) | async list(): Promise<SysDepartment[]> {
method info (line 37) | async info(id: number): Promise<DeptDetailInfo> {
method update (line 54) | async update(param: UpdateDeptDto): Promise<void> {
method transfer (line 65) | async transfer(userIds: number[], deptId: number): Promise<void> {
method add (line 75) | async add(deptName: string, parentDeptId: number): Promise<void> {
method move (line 85) | async move(depts: MoveDept[]): Promise<void> {
method delete (line 100) | async delete(departmentId: number): Promise<void> {
method countUserByDeptId (line 107) | async countUserByDeptId(id: number): Promise<number> {
method countRoleByDeptId (line 114) | async countRoleByDeptId(id: number): Promise<number> {
method countChildDept (line 121) | async countChildDept(id: number): Promise<number> {
method getDepts (line 128) | async getDepts(uid: number): Promise<SysDepartment[]> {
FILE: src/modules/admin/system/log/log.class.ts
class LoginLogInfo (line 3) | class LoginLogInfo {
class TaskLogInfo (line 23) | class TaskLogInfo {
FILE: src/modules/admin/system/log/log.controller.ts
class SysLogController (line 18) | class SysLogController {
method constructor (line 19) | constructor(private logService: SysLogService) {}
method loginLogPage (line 25) | async loginLogPage(
method taskPage (line 44) | async taskPage(
FILE: src/modules/admin/system/log/log.service.ts
class SysLogService (line 10) | class SysLogService {
method constructor (line 11) | constructor(
method saveLoginLog (line 21) | async saveLoginLog(uid: number, ip: string, ua: string): Promise<void> {
method countLoginLog (line 32) | async countLoginLog(): Promise<number> {
method pageGetLoginLog (line 39) | async pageGetLoginLog(page: number, count: number): Promise<LoginLogIn...
method clearLoginLog (line 71) | async clearLoginLog(): Promise<void> {
method recordTaskLog (line 79) | async recordTaskLog(
method countTaskLog (line 96) | async countTaskLog(): Promise<number> {
method page (line 103) | async page(page: number, count: number): Promise<TaskLogInfo[]> {
method clearTaskLog (line 135) | async clearTaskLog(): Promise<void> {
FILE: src/modules/admin/system/menu/menu.class.ts
class MenuItemAndParentInfoResult (line 4) | class MenuItemAndParentInfoResult {
FILE: src/modules/admin/system/menu/menu.controller.ts
class SysMenuController (line 29) | class SysMenuController {
method constructor (line 30) | constructor(private menuService: SysMenuService) {}
method list (line 35) | async list(@AdminUser() user: IAdminUser): Promise<SysMenu[]> {
method add (line 41) | async add(@Body() dto: CreateMenuDto): Promise<void> {
method update (line 56) | async update(@Body() dto: UpdateMenuDto): Promise<void> {
method delete (line 79) | async delete(@Body() dto: DeleteMenuDto): Promise<void> {
method info (line 97) | async info(@Query() dto: InfoMenuDto): Promise<MenuItemAndParentInfoRe...
FILE: src/modules/admin/system/menu/menu.dto.ts
class CreateMenuDto (line 17) | class CreateMenuDto {
class UpdateMenuDto (line 70) | class UpdateMenuDto extends CreateMenuDto {
class DeleteMenuDto (line 80) | class DeleteMenuDto {
class InfoMenuDto (line 90) | class InfoMenuDto {
FILE: src/modules/admin/system/menu/menu.service.ts
class SysMenuService (line 14) | class SysMenuService {
method constructor (line 15) | constructor(
method list (line 25) | async list(): Promise<SysMenu[]> {
method save (line 32) | async save(menu: CreateMenuDto & { id?: number }): Promise<void> {
method getMenus (line 39) | async getMenus(uid: number): Promise<SysMenu[]> {
method check (line 67) | async check(dto: CreateMenuDto): Promise<void | never> {
method findChildMenus (line 87) | async findChildMenus(mid: number): Promise<any> {
method getMenuItemInfo (line 109) | async getMenuItemInfo(mid: number): Promise<SysMenu> {
method getMenuItemAndParentInfo (line 117) | async getMenuItemAndParentInfo(
method findRouterExist (line 133) | async findRouterExist(router: string): Promise<boolean> {
method getPerms (line 141) | async getPerms(uid: number): Promise<string[]> {
method deleteMenuItem (line 175) | async deleteMenuItem(mids: number[]): Promise<void> {
method refreshPerms (line 182) | async refreshPerms(uid: number): Promise<void> {
method refreshOnlineUserPerms (line 196) | async refreshOnlineUserPerms(): Promise<void> {
FILE: src/modules/admin/system/online/online.class.ts
class OnlineUserInfo (line 3) | class OnlineUserInfo {
FILE: src/modules/admin/system/online/online.controller.ts
class SysOnlineController (line 20) | class SysOnlineController {
method constructor (line 21) | constructor(private onlineService: SysOnlineService) {}
method list (line 27) | async list(@AdminUser() user: IAdminUser): Promise<OnlineUserInfo[]> {
method kick (line 33) | async kick(
FILE: src/modules/admin/system/online/online.dto.ts
class KickDto (line 4) | class KickDto {
FILE: src/modules/admin/system/online/online.service.ts
class SysOnlineService (line 14) | class SysOnlineService {
method constructor (line 15) | constructor(
method listOnlineUser (line 25) | async listOnlineUser(currentUid: number): Promise<OnlineUserInfo[]> {
method kickUser (line 40) | async kickUser(uid: number, currentUid: number): Promise<void> {
method findSocketIdByUid (line 63) | async findSocketIdByUid(uid: number): Promise<RemoteSocket<any, any>> {
method findLastLoginInfoList (line 76) | async findLastLoginInfoList(
FILE: src/modules/admin/system/param-config/param-config.controller.ts
class SysParamConfigController (line 23) | class SysParamConfigController {
method constructor (line 24) | constructor(private paramConfigService: SysParamConfigService) {}
method page (line 29) | async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysConfig...
method add (line 47) | async add(@Body() dto: CreateParamConfigDto): Promise<void> {
method info (line 55) | async info(@Query() dto: InfoParamConfigDto): Promise<SysConfig> {
method update (line 61) | async update(@Body() dto: UpdateParamConfigDto): Promise<void> {
method delete (line 67) | async delete(@Body() dto: DeleteParamConfigDto): Promise<void> {
FILE: src/modules/admin/system/param-config/param-config.dto.ts
class CreateParamConfigDto (line 13) | class CreateParamConfigDto {
class UpdateParamConfigDto (line 33) | class UpdateParamConfigDto {
class DeleteParamConfigDto (line 53) | class DeleteParamConfigDto {
class InfoParamConfigDto (line 60) | class InfoParamConfigDto {
FILE: src/modules/admin/system/param-config/param-config.service.ts
class SysParamConfigService (line 9) | class SysParamConfigService {
method constructor (line 10) | constructor(
method getConfigListByPage (line 18) | async getConfigListByPage(page: number, count: number): Promise<SysCon...
method countConfigList (line 31) | async countConfigList(): Promise<number> {
method add (line 38) | async add(dto: CreateParamConfigDto): Promise<void> {
method update (line 45) | async update(dto: UpdateParamConfigDto): Promise<void> {
method delete (line 55) | async delete(ids: number[]): Promise<void> {
method findOne (line 62) | async findOne(id: number): Promise<SysConfig> {
method isExistKey (line 66) | async isExistKey(key: string): Promise<void | never> {
method findValueByKey (line 73) | async findValueByKey(key: string): Promise<string | null> {
FILE: src/modules/admin/system/role/role.class.ts
class RoleInfo (line 6) | class RoleInfo {
class CreatedRoleId (line 23) | class CreatedRoleId {
FILE: src/modules/admin/system/role/role.controller.ts
class SysRoleController (line 28) | class SysRoleController {
method constructor (line 29) | constructor(
method list (line 37) | async list(): Promise<SysRole[]> {
method page (line 44) | async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysRole>> {
method delete (line 59) | async delete(@Body() dto: DeleteRoleDto): Promise<void> {
method add (line 70) | async add(
method update (line 79) | async update(@Body() dto: UpdateRoleDto): Promise<void> {
method info (line 87) | async info(@Query() dto: InfoRoleDto): Promise<RoleInfo> {
FILE: src/modules/admin/system/role/role.dto.ts
class DeleteRoleDto (line 14) | class DeleteRoleDto {
class CreateRoleDto (line 24) | class CreateRoleDto {
class UpdateRoleDto (line 64) | class UpdateRoleDto extends CreateRoleDto {
class InfoRoleDto (line 73) | class InfoRoleDto {
FILE: src/modules/admin/system/role/role.service.ts
class SysRoleService (line 14) | class SysRoleService {
method constructor (line 15) | constructor(
method list (line 30) | async list(): Promise<SysRole[]> {
method count (line 40) | async count(): Promise<number> {
method info (line 50) | async info(rid: number): Promise<RoleInfo> {
method delete (line 64) | async delete(roleIds: number[]): Promise<void> {
method add (line 78) | async add(param: CreateRoleDto, uid: number): Promise<CreatedRoleId> {
method update (line 114) | async update(param: UpdateRoleDto): Promise<SysRole> {
method page (line 188) | async page(page: number, count: number): Promise<SysRole[]> {
method getRoleIdByUser (line 205) | async getRoleIdByUser(id: number): Promise<number[]> {
method countUserIdByRole (line 222) | async countUserIdByRole(ids: number[]): Promise<number | never> {
FILE: src/modules/admin/system/serve/serve.class.ts
class Runtime (line 3) | class Runtime {
class CoreLoad (line 17) | class CoreLoad {
class Cpu (line 26) | class Cpu {
class Disk (line 52) | class Disk {
class Memory (line 63) | class Memory {
class ServeStatInfo (line 74) | class ServeStatInfo {
FILE: src/modules/admin/system/serve/serve.controller.ts
class SysServeController (line 16) | class SysServeController {
method constructor (line 17) | constructor(private serveService: SysServeService) {}
method stat (line 23) | async stat(): Promise<ServeStatInfo> {
FILE: src/modules/admin/system/serve/serve.service.ts
class SysServeService (line 6) | class SysServeService {
method getServeStat (line 10) | async getServeStat(): Promise<ServeStatInfo> {
FILE: src/modules/admin/system/system.module.ts
class SystemModule (line 107) | class SystemModule {}
FILE: src/modules/admin/system/task/task.controller.ts
class SysTaskController (line 20) | class SysTaskController {
method constructor (line 21) | constructor(private taskService: SysTaskService) {}
method page (line 26) | async page(@Query() dto: PageOptionsDto): Promise<PageResult<SysTask>> {
method add (line 41) | async add(@Body() dto: CreateTaskDto): Promise<void> {
method update (line 49) | async update(@Body() dto: UpdateTaskDto): Promise<void> {
method info (line 58) | async info(@Query() dto: CheckIdTaskDto): Promise<SysTask> {
method once (line 64) | async once(@Body() dto: CheckIdTaskDto): Promise<void> {
method stop (line 75) | async stop(@Body() dto: CheckIdTaskDto): Promise<void> {
method start (line 86) | async start(@Body() dto: CheckIdTaskDto): Promise<void> {
method delete (line 97) | async delete(@Body() dto: CheckIdTaskDto): Promise<void> {
FILE: src/modules/admin/system/task/task.dto.ts
class IsCronExpression (line 23) | class IsCronExpression implements ValidatorConstraintInterface {
method validate (line 25) | validate(value: string, args: ValidationArguments) {
method defaultMessage (line 38) | defaultMessage(_args: ValidationArguments) {
class CreateTaskDto (line 44) | class CreateTaskDto {
class UpdateTaskDto (line 101) | class UpdateTaskDto extends CreateTaskDto {
class CheckIdTaskDto (line 108) | class CheckIdTaskDto {
FILE: src/modules/admin/system/task/task.processor.ts
type ExecuteData (line 7) | interface ExecuteData {
class SysTaskConsumer (line 14) | class SysTaskConsumer {
method constructor (line 15) | constructor(
method handle (line 21) | async handle(job: Job<ExecuteData>): Promise<void> {
method onCompleted (line 37) | onCompleted(job: Job<ExecuteData>) {
FILE: src/modules/admin/system/task/task.service.ts
class SysTaskService (line 21) | class SysTaskService implements OnModuleInit {
method constructor (line 22) | constructor(
method onModuleInit (line 34) | async onModuleInit() {
method initTask (line 41) | async initTask(): Promise<void> {
method page (line 81) | async page(page: number, count: number): Promise<SysTask[]> {
method count (line 95) | async count(): Promise<number> {
method info (line 102) | async info(id: number): Promise<SysTask> {
method delete (line 109) | async delete(task: SysTask): Promise<void> {
method once (line 120) | async once(task: SysTask): Promise<void | never> {
method addOrUpdate (line 134) | async addOrUpdate(param: CreateTaskDto | UpdateTaskDto): Promise<void> {
method start (line 147) | async start(task: SysTask): Promise<void> {
method stop (line 195) | async stop(task: SysTask): Promise<void> {
method existJob (line 228) | async existJob(jobId: string): Promise<boolean> {
method updateTaskCompleteStatus (line 240) | async updateTaskCompleteStatus(tid: number): Promise<void> {
method checkHasMissionMeta (line 258) | async checkHasMissionMeta(
method callService (line 297) | async callService(serviceName: string, args: string): Promise<void> {
method safeParse (line 323) | safeParse(args: string): unknown | string {
FILE: src/modules/admin/system/user/user.class.ts
class AccountInfo (line 4) | class AccountInfo {
class PageSearchUserInfo (line 24) | class PageSearchUserInfo {
class UserDetailInfo (line 70) | class UserDetailInfo extends SysUser {
FILE: src/modules/admin/system/user/user.controller.ts
class SysUserController (line 27) | class SysUserController {
method constructor (line 28) | constructor(
method add (line 37) | async add(@Body() dto: CreateUserDto): Promise<void> {
method info (line 46) | async info(@Query() dto: InfoUserDto): Promise<UserDetailInfo> {
method delete (line 54) | async delete(@Body() dto: DeleteUserDto): Promise<void> {
method page (line 64) | async page(
method update (line 89) | async update(@Body() dto: UpdateUserDto): Promise<void> {
method password (line 98) | async password(@Body() dto: PasswordUserDto): Promise<void> {
FILE: src/modules/admin/system/user/user.dto.ts
class UpdateUserInfoDto (line 22) | class UpdateUserInfoDto {
class UpdatePasswordDto (line 56) | class UpdatePasswordDto {
class CreateUserDto (line 73) | class CreateUserDto {
class UpdateUserDto (line 145) | class UpdateUserDto extends CreateUserDto {
class InfoUserDto (line 154) | class InfoUserDto {
class DeleteUserDto (line 164) | class DeleteUserDto {
class PageSearchUserDto (line 174) | class PageSearchUserDto extends PageOptionsDto {
class PasswordUserDto (line 186) | class PasswordUserDto {
FILE: src/modules/admin/system/user/user.service.ts
class SysUserService (line 23) | class SysUserService {
method constructor (line 24) | constructor(
method findUserByUserName (line 40) | async findUserByUserName(username: string): Promise<SysUser | undefine...
method getAccountInfo (line 53) | async getAccountInfo(uid: number): Promise<AccountInfo> {
method updatePersonInfo (line 73) | async updatePersonInfo(uid: number, info: UpdateUserInfoDto): Promise<...
method updatePassword (line 80) | async updatePassword(uid: number, dto: UpdatePasswordDto): Promise<voi...
method forceUpdatePassword (line 98) | async forceUpdatePassword(uid: number, password: string): Promise<void> {
method add (line 112) | async add(param: CreateUserDto): Promise<void> {
method update (line 158) | async update(param: UpdateUserDto): Promise<void> {
method info (line 191) | async info(
method infoList (line 217) | async infoList(ids: number[]): Promise<SysUser[]> {
method delete (line 225) | async delete(userIds: number[]): Promise<void | never> {
method count (line 237) | async count(uid: number, deptIds: number[]): Promise<number> {
method findRootUserId (line 256) | async findRootUserId(): Promise<number> {
method page (line 267) | async page(
method forbidden (line 328) | async forbidden(uid: number): Promise<void> {
method multiForbidden (line 337) | async multiForbidden(uids: number[]): Promise<void> {
method upgradePasswordV (line 356) | async upgradePasswordV(id: number): Promise<void> {
FILE: src/modules/ws/admin-ws.gateway.ts
class AdminWSGateway (line 19) | class AdminWSGateway
method socketServer (line 25) | get socketServer(): Server {
method constructor (line 29) | constructor(private authService: AuthService) {}
method afterInit (line 35) | afterInit() {
method handleConnection (line 42) | async handleConnection(client: Socket): Promise<void> {
method handleDisconnect (line 58) | async handleDisconnect(client: Socket): Promise<void> {
FILE: src/modules/ws/admin-ws.guard.ts
class AdminWsGuard (line 8) | class AdminWsGuard implements CanActivate {
method constructor (line 9) | constructor(private authService: AuthService) {}
method canActivate (line 11) | canActivate(
FILE: src/modules/ws/auth.service.ts
class AuthService (line 8) | class AuthService {
method constructor (line 9) | constructor(private jwtService: JwtService) {}
method checkAdminAuthToken (line 11) | checkAdminAuthToken(
FILE: src/modules/ws/ws.event.ts
constant EVENT_ONLINE (line 2) | const EVENT_ONLINE = 'online';
constant EVENT_OFFLINE (line 3) | const EVENT_OFFLINE = 'offline';
constant EVENT_KICK (line 5) | const EVENT_KICK = 'kick';
FILE: src/modules/ws/ws.module.ts
class WSModule (line 14) | class WSModule {}
FILE: src/setup-swagger.ts
function setupSwagger (line 7) | function setupSwagger(app: INestApplication): void {
FILE: src/shared/logger/logger.constants.ts
constant LOGGER_MODULE_OPTIONS (line 1) | const LOGGER_MODULE_OPTIONS = Symbol('LOGGER_MODULE_OPTIONS');
constant PROJECT_LOG_DIR_NAME (line 2) | const PROJECT_LOG_DIR_NAME = 'logs';
constant DEFAULT_WEB_LOG_NAME (line 3) | const DEFAULT_WEB_LOG_NAME = 'web.log';
constant DEFAULT_ERROR_LOG_NAME (line 4) | const DEFAULT_ERROR_LOG_NAME = 'common-error.log';
constant DEFAULT_ACCESS_LOG_NAME (line 5) | const DEFAULT_ACCESS_LOG_NAME = 'access.log';
constant DEFAULT_SQL_SLOW_LOG_NAME (line 6) | const DEFAULT_SQL_SLOW_LOG_NAME = 'sql-slow.log';
constant DEFAULT_SQL_ERROR_LOG_NAME (line 7) | const DEFAULT_SQL_ERROR_LOG_NAME = 'sql-error.log';
constant DEFAULT_TASK_LOG_NAME (line 8) | const DEFAULT_TASK_LOG_NAME = 'task.log';
constant DEFAULT_MAX_SIZE (line 10) | const DEFAULT_MAX_SIZE = '2m';
FILE: src/shared/logger/logger.interface.ts
type WinstonLogLevel (line 7) | type WinstonLogLevel = 'info' | 'error' | 'warn' | 'debug' | 'verbose';
type TypeORMLoggerOptions (line 9) | interface TypeORMLoggerOptions {
type LoggerModuleOptions (line 16) | interface LoggerModuleOptions {
type LoggerModuleAsyncOptions (line 71) | interface LoggerModuleAsyncOptions
FILE: src/shared/logger/logger.module.ts
class LoggerModule (line 10) | class LoggerModule {
method forRoot (line 11) | static forRoot(
method forRootAsync (line 29) | static forRootAsync(
FILE: src/shared/logger/logger.service.ts
constant DEFAULT_LOG_CONSOLE_LEVELS (line 26) | const DEFAULT_LOG_CONSOLE_LEVELS: WinstonLogLevel = isDev() ? 'info' : '...
constant DEFAULT_LOG_WINSTON_LEVELS (line 27) | const DEFAULT_LOG_WINSTON_LEVELS: WinstonLogLevel = 'info';
constant LOG_LEVEL_VALUES (line 33) | const LOG_LEVEL_VALUES: Record<WinstonLogLevel, number> = {
class LoggerService (line 42) | class LoggerService implements NestLoggerService {
method constructor (line 56) | constructor(
method initWinston (line 84) | private initWinston() {
method getLogDir (line 123) | protected getLogDir(): string {
method getWinstonLogger (line 130) | protected getWinstonLogger(): WinstonLogger {
method log (line 140) | log(message: any, ...optionalParams: any[]) {
method error (line 163) | error(message: any, ...optionalParams: any[]) {
method warn (line 184) | warn(message: any, ...optionalParams: any[]) {
method debug (line 206) | debug(message: any, ...optionalParams: any[]) {
method verbose (line 228) | verbose(message: any, ...optionalParams: any[]) {
method isConsoleLevelEnabled (line 244) | protected isConsoleLevelEnabled(level: WinstonLogLevel): boolean {
method isWinstonLevelEnabled (line 255) | protected isWinstonLevelEnabled(level: WinstonLogLevel): boolean {
method getTimestamp (line 264) | protected getTimestamp(): string {
method recordMessages (line 279) | protected recordMessages(
method printMessages (line 304) | protected printMessages(
method printStackTrace (line 331) | protected printStackTrace(stack: string) {
method updateAndGetTimestampDiff (line 338) | private updateAndGetTimestampDiff(): string {
method getContextAndMessagesToPrint (line 348) | private getContextAndMessagesToPrint(args: unknown[]) {
method getContextAndStackAndMessagesToPrint (line 363) | private getContextAndStackAndMessagesToPrint(args: unknown[]) {
method getColorByLogLevel (line 380) | private getColorByLogLevel(level: WinstonLogLevel): (text: string) => ...
FILE: src/shared/logger/typeorm-logger.service.ts
class TypeORMLoggerService (line 14) | class TypeORMLoggerService implements Logger {
method constructor (line 20) | constructor(
method logQuery (line 39) | logQuery(query: string, parameters?: any[]) {
method logQueryError (line 57) | logQueryError(error: string | Error, query: string, parameters?: any[]) {
method logQuerySlow (line 75) | logQuerySlow(time: number, query: string, parameters?: any[]) {
method logSchemaBuild (line 87) | logSchemaBuild(message: string) {
method logMigration (line 99) | logMigration(message: string) {
method log (line 107) | log(level: 'log' | 'info' | 'warn', message: any) {
method stringifyParams (line 137) | protected stringifyParams(parameters: any[]) {
FILE: src/shared/logger/utils/app-root-path.util.ts
function getAppRootPath (line 8) | function getAppRootPath(): string {
FILE: src/shared/logger/utils/home-dir.ts
function getHomedir (line 3) | function getHomedir(): string {
FILE: src/shared/redis/redis.constants.ts
constant REDIS_CLIENT (line 1) | const REDIS_CLIENT = Symbol('REDIS_CLIENT');
constant REDIS_MODULE_OPTIONS (line 2) | const REDIS_MODULE_OPTIONS = Symbol('REDIS_MODULE_OPTIONS');
constant REDIS_DEFAULT_CLIENT_KEY (line 3) | const REDIS_DEFAULT_CLIENT_KEY = 'default';
FILE: src/shared/redis/redis.interface.ts
type RedisModuleOptions (line 4) | interface RedisModuleOptions extends RedisOptions {
type RedisModuleAsyncOptions (line 36) | interface RedisModuleAsyncOptions
FILE: src/shared/redis/redis.module.ts
class RedisModule (line 17) | class RedisModule implements OnModuleDestroy {
method register (line 18) | static register(
method registerAsync (line 35) | static registerAsync(options: RedisModuleAsyncOptions): DynamicModule {
method createAysncProvider (line 48) | private static createAysncProvider(): Provider {
method createClient (line 77) | private static createClient(options: RedisModuleOptions): Redis | Clus...
method createAsyncClientOptions (line 96) | private static createAsyncClientOptions(options: RedisModuleAsyncOptio...
method onModuleDestroy (line 104) | onModuleDestroy() {
FILE: src/shared/services/redis.service.ts
class RedisService (line 10) | class RedisService {
method constructor (line 11) | constructor(
method getRedis (line 21) | public getRedis(name = REDIS_DEFAULT_CLIENT_KEY): Redis {
FILE: src/shared/services/util.service.ts
class UtilService (line 7) | class UtilService {
method getReqIP (line 11) | getReqIP(req: FastifyRequest): string {
method aesEncrypt (line 25) | public aesEncrypt(msg: string, secret: string): string {
method aesDecrypt (line 32) | public aesDecrypt(encrypted: string, secret: string): string {
method md5 (line 39) | public md5(msg: string): string {
method generateUUID (line 46) | public generateUUID(): string {
method generateRandomValue (line 53) | public generateRandomValue(
FILE: src/shared/shared.module.ts
class SharedModule (line 46) | class SharedModule {}
Condensed preview — 139 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (309K chars).
[
{
"path": ".dockerignore",
"chars": 473,
"preview": "# 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"
},
{
"path": ".eslintrc.js",
"chars": 631,
"preview": "module.exports = {\n parser: '@typescript-eslint/parser',\n parserOptions: {\n project: 'tsconfig.json',\n sourceTyp"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 685,
"preview": "---\nname: Bug report(报告问题)\nabout: Create a report to help us improve\n---\n<!--\n 注意:为更好的解决你的问题,请参考模板提供完整信息,准确描述问题,信息不全的"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 105,
"preview": "---\nname: Feature Request(新功能建议)\nabout: Suggest an idea for this project\n---\n\n## Feature request(新功能建议)\n\n"
},
{
"path": ".github/workflows/build-rc.yml",
"chars": 771,
"preview": "name: Build RC Image\n\non:\n create:\n tags:\n - '*'\n\njobs:\n docker:\n runs-on: ubuntu-latest\n steps:\n -"
},
{
"path": ".github/workflows/build-stable.yml",
"chars": 784,
"preview": "name: Build Stable Image\n\non:\n push:\n branches:\n - 'main'\n\njobs:\n docker:\n runs-on: ubuntu-latest\n steps"
},
{
"path": ".gitignore",
"chars": 462,
"preview": "# 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"
},
{
"path": ".prettierrc",
"chars": 68,
"preview": "{\n \"tabWidth\": 2,\n \"singleQuote\": true,\n \"trailingComma\": \"all\"\n}"
},
{
"path": "Dockerfile",
"chars": 516,
"preview": "FROM node:lts-alpine as builder\nWORKDIR /sf-nest-admin\n\n# set timezone\nRUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "MIT License\n\nCopyright (c) 2021-present Changyuan Yang\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 1153,
"preview": "# sf-nest-admin\n\n  => {\n "
},
{
"path": "src/config/configuration.ts",
"chars": 435,
"preview": "import { merge } from 'lodash';\nimport DefaultConfig from './config.default';\nimport { IConfig } from './defineConfig';\n"
},
{
"path": "src/config/defineConfig.ts",
"chars": 1471,
"preview": "import { conf } from 'qiniu';\nimport { LoggerModuleOptions as LoggerConfigOptions } from 'src/shared/logger/logger.inter"
},
{
"path": "src/config/env.ts",
"chars": 143,
"preview": "/**\n * check dev env\n * @returns boolean true is dev\n */\nexport function isDev(): boolean {\n return process.env.NODE_EN"
},
{
"path": "src/entities/admin/sys-config.entity.ts",
"chars": 636,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-department.entity.ts",
"chars": 557,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-login-log.entity.ts",
"chars": 609,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-menu.entity.ts",
"chars": 1122,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-role-department.entity.ts",
"chars": 467,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-role-menu.entity.ts",
"chars": 443,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-role.entity.ts",
"chars": 570,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport "
},
{
"path": "src/entities/admin/sys-task-log.entity.ts",
"chars": 651,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-task.entity.ts",
"chars": 1277,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-user-role.entity.ts",
"chars": 443,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/admin/sys-user.entity.ts",
"chars": 1065,
"preview": "import { PrimaryGeneratedColumn, Column, Entity } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\nimport "
},
{
"path": "src/entities/base.entity.ts",
"chars": 308,
"preview": "import { CreateDateColumn, UpdateDateColumn } from 'typeorm';\nimport { ApiProperty } from '@nestjs/swagger';\n\nexport abs"
},
{
"path": "src/main.ts",
"chars": 1817,
"preview": "import {\n HttpStatus,\n UnprocessableEntityException,\n ValidationPipe,\n} from '@nestjs/common';\nimport { NestFactory, "
},
{
"path": "src/mission/README.md",
"chars": 157,
"preview": "### 任务注册\n\n在jobs下定义任务,并在`mission.module.ts`中的providers中注册即可。\n\n### 添加任务\n\n在后台页面中的定时任务页面中进行添加,主要识别为**服务路径**的定义:`Job类名.方法名`,填"
},
{
"path": "src/mission/jobs/http-request.job.ts",
"chars": 731,
"preview": "import { HttpService } from '@nestjs/axios';\nimport { Injectable } from '@nestjs/common';\nimport { LoggerService } from "
},
{
"path": "src/mission/jobs/sys-log-clear.job.ts",
"chars": 484,
"preview": "import { Injectable } from '@nestjs/common';\nimport { SysLogService } from 'src/modules/admin/system/log/log.service';\ni"
},
{
"path": "src/mission/mission.decorator.ts",
"chars": 245,
"preview": "import { SetMetadata } from '@nestjs/common';\nimport { MISSION_KEY_METADATA } from '../common/contants/decorator.contant"
},
{
"path": "src/mission/mission.module.ts",
"chars": 1168,
"preview": "import { DynamicModule, ExistingProvider, Module } from '@nestjs/common';\nimport { AdminModule } from 'src/modules/admin"
},
{
"path": "src/modules/admin/account/account.controller.ts",
"chars": 2199,
"preview": "import { Body, Controller, Get, Post } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecurity,\n "
},
{
"path": "src/modules/admin/account/account.dto.ts",
"chars": 582,
"preview": "import { Optional } from '@nestjs/common';\nimport { ApiProperty } from '@nestjs/swagger';\nimport { IsString } from 'clas"
},
{
"path": "src/modules/admin/account/account.module.ts",
"chars": 328,
"preview": "import { Module } from '@nestjs/common';\nimport { LoginModule } from '../login/login.module';\nimport { SystemModule } fr"
},
{
"path": "src/modules/admin/admin.constants.ts",
"chars": 847,
"preview": "export const ADMIN_USER = 'adminUser';\nexport const AUTHORIZE_KEY_METADATA = 'admin_module:authorize';\nexport const PERM"
},
{
"path": "src/modules/admin/admin.interface.ts",
"chars": 314,
"preview": "import { conf } from 'qiniu';\n\nexport interface IAdminUser {\n uid: number;\n pv: number;\n}\n\nexport type QINIU_ACCESS_CO"
},
{
"path": "src/modules/admin/admin.module.ts",
"chars": 1140,
"preview": "import { Module } from '@nestjs/common';\nimport { APP_GUARD, RouterModule } from '@nestjs/core';\nimport { AccountModule "
},
{
"path": "src/modules/admin/core/decorators/admin-user.decorator.ts",
"chars": 391,
"preview": "import { createParamDecorator, ExecutionContext } from '@nestjs/common';\nimport { ADMIN_USER } from '../../admin.constan"
},
{
"path": "src/modules/admin/core/decorators/authorize.decorator.ts",
"chars": 223,
"preview": "import { SetMetadata } from '@nestjs/common';\nimport { AUTHORIZE_KEY_METADATA } from '../../admin.constants';\n\n/**\n * 开放"
},
{
"path": "src/modules/admin/core/decorators/log-disabled.decorator.ts",
"chars": 211,
"preview": "import { SetMetadata } from '@nestjs/common';\nimport { LOG_DISABLED_KEY_METADATA } from '../../admin.constants';\n\n/**\n *"
},
{
"path": "src/modules/admin/core/decorators/permission-optional.decorator.ts",
"chars": 266,
"preview": "import { SetMetadata } from '@nestjs/common';\nimport { PERMISSION_OPTIONAL_KEY_METADATA } from '../../admin.constants';\n"
},
{
"path": "src/modules/admin/core/guards/auth.guard.ts",
"chars": 2748,
"preview": "import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';\nimport { Reflector } from '@nestjs/core';\nim"
},
{
"path": "src/modules/admin/core/provider/qiniu.provider.ts",
"chars": 768,
"preview": "import { FactoryProvider } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { QINIU_CONFIG "
},
{
"path": "src/modules/admin/core/provider/root-role-id.provider.ts",
"chars": 464,
"preview": "import { FactoryProvider } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { ROOT_ROLE_ID "
},
{
"path": "src/modules/admin/login/login.class.ts",
"chars": 552,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport SysMenu from 'src/entities/admin/sys-menu.entity';\n\nexport class I"
},
{
"path": "src/modules/admin/login/login.controller.ts",
"chars": 1524,
"preview": "import {\n Body,\n Controller,\n Get,\n Headers,\n Post,\n Query,\n Req,\n} from '@nestjs/common';\nimport { FastifyReques"
},
{
"path": "src/modules/admin/login/login.dto.ts",
"chars": 960,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n IsInt,\n IsOptional,"
},
{
"path": "src/modules/admin/login/login.module.ts",
"chars": 358,
"preview": "import { Module } from '@nestjs/common';\nimport { SystemModule } from '../system/system.module';\nimport { LoginControlle"
},
{
"path": "src/modules/admin/login/login.service.ts",
"chars": 3998,
"preview": "import { Injectable } from '@nestjs/common';\nimport * as svgCaptcha from 'svg-captcha';\nimport { ImageCaptcha, PermMenuI"
},
{
"path": "src/modules/admin/netdisk/manager/manage.class.ts",
"chars": 1437,
"preview": "import { ApiProperty } from '@nestjs/swagger';\n\nexport type FileType = 'file' | 'dir';\n\nexport class SFileInfo {\n @ApiP"
},
{
"path": "src/modules/admin/netdisk/manager/manage.controller.ts",
"chars": 3664,
"preview": "import {\n ApiOkResponse,\n ApiOperation,\n ApiSecurity,\n ApiTags,\n} from '@nestjs/swagger';\nimport { NetDiskManageServ"
},
{
"path": "src/modules/admin/netdisk/manager/manage.dto.ts",
"chars": 3645,
"preview": "import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n "
},
{
"path": "src/modules/admin/netdisk/manager/manage.service.ts",
"chars": 26768,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport {\n NETDISK_COPY_SUFFIX,\n NETDISK_DELIMITER,\n NETDISK_HAND"
},
{
"path": "src/modules/admin/netdisk/netdisk.module.ts",
"chars": 662,
"preview": "import { Module } from '@nestjs/common';\nimport { qiniuProvider } from '../core/provider/qiniu.provider';\nimport { Syste"
},
{
"path": "src/modules/admin/netdisk/overview/overview.class.ts",
"chars": 1262,
"preview": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class SpaceInfo {\n @ApiProperty({ description: '当月的X号', type: [N"
},
{
"path": "src/modules/admin/netdisk/overview/overview.controller.ts",
"chars": 1503,
"preview": "import {\n CacheInterceptor,\n CacheKey,\n CacheTTL,\n Controller,\n Get,\n UseInterceptors,\n} from '@nestjs/common';\nim"
},
{
"path": "src/modules/admin/netdisk/overview/overview.service.ts",
"chars": 4999,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport { QINIU_API, QINIU_CONFIG } from '../../admin.constants';\nim"
},
{
"path": "src/modules/admin/system/dept/dept.class.ts",
"chars": 302,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport SysDepartment from 'src/entities/admin/sys-department.entity';\n\nex"
},
{
"path": "src/modules/admin/system/dept/dept.controller.ts",
"chars": 2810,
"preview": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecu"
},
{
"path": "src/modules/admin/system/dept/dept.dto.ts",
"chars": 1553,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n ArrayNotEmpty,\n IsA"
},
{
"path": "src/modules/admin/system/dept/dept.service.ts",
"chars": 4230,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectEntityManager, InjectRepository } from '@nestjs/type"
},
{
"path": "src/modules/admin/system/log/log.class.ts",
"chars": 860,
"preview": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class LoginLogInfo {\n @ApiProperty({ description: '日志编号' })\n id"
},
{
"path": "src/modules/admin/system/log/log.controller.ts",
"chars": 1619,
"preview": "import { Controller, Get, Query } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecurity,\n ApiT"
},
{
"path": "src/modules/admin/system/log/log.service.ts",
"chars": 3422,
"preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport SysLoginLog from"
},
{
"path": "src/modules/admin/system/menu/menu.class.ts",
"chars": 272,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport SysMenu from 'src/entities/admin/sys-menu.entity';\n\nexport class M"
},
{
"path": "src/modules/admin/system/menu/menu.controller.ts",
"chars": 2937,
"preview": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecu"
},
{
"path": "src/modules/admin/system/menu/menu.dto.ts",
"chars": 1821,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n IsBoolean,\n IsIn,\n "
},
{
"path": "src/modules/admin/system/menu/menu.service.ts",
"chars": 5905,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { concat"
},
{
"path": "src/modules/admin/system/online/online.class.ts",
"chars": 549,
"preview": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class OnlineUserInfo {\n @ApiProperty({ description: '最近的一条登录日志ID"
},
{
"path": "src/modules/admin/system/online/online.controller.ts",
"chars": 1331,
"preview": "import { Body, Controller, Get, Post } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecurity,\n "
},
{
"path": "src/modules/admin/system/online/online.dto.ts",
"chars": 184,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsInt } from 'class-validator';\n\nexport class KickDto {\n @ApiPr"
},
{
"path": "src/modules/admin/system/online/online.service.ts",
"chars": 3611,
"preview": "import { Injectable } from '@nestjs/common';\nimport { JwtService } from '@nestjs/jwt';\nimport { InjectEntityManager } fr"
},
{
"path": "src/modules/admin/system/param-config/param-config.controller.ts",
"chars": 2071,
"preview": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecu"
},
{
"path": "src/modules/admin/system/param-config/param-config.dto.ts",
"chars": 1227,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n ArrayNotEmpty,\n IsA"
},
{
"path": "src/modules/admin/system/param-config/param-config.service.ts",
"chars": 1920,
"preview": "import { Injectable } from '@nestjs/common';\nimport { InjectRepository } from '@nestjs/typeorm';\nimport { ApiException }"
},
{
"path": "src/modules/admin/system/role/role.class.ts",
"chars": 546,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport SysRoleDepartment from 'src/entities/admin/sys-role-department.ent"
},
{
"path": "src/modules/admin/system/role/role.controller.ts",
"chars": 2627,
"preview": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n ApiOperation,\n ApiOkResponse,\n ApiSecu"
},
{
"path": "src/modules/admin/system/role/role.dto.ts",
"chars": 1255,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n ArrayNotEmpty,\n IsA"
},
{
"path": "src/modules/admin/system/role/role.service.ts",
"chars": 6497,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectEntityManager, InjectRepository } from '@nestjs/type"
},
{
"path": "src/modules/admin/system/serve/serve.class.ts",
"chars": 1872,
"preview": "import { ApiProperty } from '@nestjs/swagger';\n\nexport class Runtime {\n @ApiProperty({ description: '系统' })\n os?: stri"
},
{
"path": "src/modules/admin/system/serve/serve.controller.ts",
"chars": 764,
"preview": "import { Controller, Get } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecurity,\n ApiTags,\n} "
},
{
"path": "src/modules/admin/system/serve/serve.service.ts",
"chars": 1541,
"preview": "import { Injectable } from '@nestjs/common';\nimport * as si from 'systeminformation';\nimport { Disk, ServeStatInfo } fro"
},
{
"path": "src/modules/admin/system/system.module.ts",
"chars": 3737,
"preview": "import { Module } from '@nestjs/common';\nimport { TypeOrmModule } from '@nestjs/typeorm';\nimport {\n ROOT_ROLE_ID,\n SYS"
},
{
"path": "src/modules/admin/system/task/task.controller.ts",
"chars": 3174,
"preview": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecu"
},
{
"path": "src/modules/admin/system/task/task.dto.ts",
"chars": 2645,
"preview": "import * as parser from 'cron-parser';\nimport { isEmpty } from 'lodash';\nimport {\n IsDateString,\n IsIn,\n IsInt,\n IsO"
},
{
"path": "src/modules/admin/system/task/task.processor.ts",
"chars": 1144,
"preview": "import { OnQueueCompleted, Process, Processor } from '@nestjs/bull';\nimport { Job } from 'bull';\nimport { SYS_TASK_QUEUE"
},
{
"path": "src/modules/admin/system/task/task.service.ts",
"chars": 8809,
"preview": "import { InjectQueue } from '@nestjs/bull';\nimport { Injectable, OnModuleInit } from '@nestjs/common';\nimport { ModuleRe"
},
{
"path": "src/modules/admin/system/user/user.class.ts",
"chars": 1127,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport SysUser from 'src/entities/admin/sys-user.entity';\n\nexport class A"
},
{
"path": "src/modules/admin/system/user/user.controller.ts",
"chars": 2551,
"preview": "import { Body, Controller, Get, Post, Query } from '@nestjs/common';\nimport {\n ApiOkResponse,\n ApiOperation,\n ApiSecu"
},
{
"path": "src/modules/admin/system/user/user.dto.ts",
"chars": 3236,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { Type } from 'class-transformer';\nimport {\n ArrayMaxSize,\n Arra"
},
{
"path": "src/modules/admin/system/user/user.service.ts",
"chars": 10624,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport { InjectEntityManager, InjectRepository } from '@nestjs/type"
},
{
"path": "src/modules/ws/admin-ws.gateway.ts",
"chars": 1275,
"preview": "import {\n OnGatewayConnection,\n OnGatewayDisconnect,\n OnGatewayInit,\n WebSocketGateway,\n WebSocketServer,\n} from '@"
},
{
"path": "src/modules/ws/admin-ws.guard.ts",
"chars": 832,
"preview": "import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';\nimport { Observable } from 'rxjs';\nimport { "
},
{
"path": "src/modules/ws/auth.service.ts",
"chars": 704,
"preview": "import { Injectable } from '@nestjs/common';\nimport { JwtService } from '@nestjs/jwt';\nimport { isEmpty } from 'lodash';"
},
{
"path": "src/modules/ws/ws.event.ts",
"chars": 129,
"preview": "// 用户上线事件\nexport const EVENT_ONLINE = 'online';\nexport const EVENT_OFFLINE = 'offline';\n// 踢下线\nexport const EVENT_KICK ="
},
{
"path": "src/modules/ws/ws.module.ts",
"chars": 292,
"preview": "import { Module } from '@nestjs/common';\nimport { AdminWSGateway } from './admin-ws.gateway';\nimport { AuthService } fro"
},
{
"path": "src/polyfill.ts",
"chars": 123,
"preview": "import { format } from 'date-fns';\n\nDate.prototype.toJSON = function () {\n return format(this, 'yyyy-MM-dd HH:mm:ss');\n"
},
{
"path": "src/setup-swagger.ts",
"chars": 1073,
"preview": "import { INestApplication } from '@nestjs/common';\nimport { ConfigService } from '@nestjs/config';\nimport { DocumentBuil"
},
{
"path": "src/shared/logger/logger.constants.ts",
"chars": 487,
"preview": "export const LOGGER_MODULE_OPTIONS = Symbol('LOGGER_MODULE_OPTIONS');\nexport const PROJECT_LOG_DIR_NAME = 'logs';\nexport"
},
{
"path": "src/shared/logger/logger.interface.ts",
"chars": 1689,
"preview": "import { ModuleMetadata } from '@nestjs/common';\nimport { LoggerOptions } from 'typeorm';\n\n/**\n * 日志等级\n */\nexport type W"
},
{
"path": "src/shared/logger/logger.module.ts",
"chars": 1126,
"preview": "import { DynamicModule, Module } from '@nestjs/common';\nimport { LOGGER_MODULE_OPTIONS } from './logger.constants';\nimpo"
},
{
"path": "src/shared/logger/logger.service.ts",
"chars": 11768,
"preview": "import {\n Injectable,\n Optional,\n Inject,\n LoggerService as NestLoggerService,\n} from '@nestjs/common';\nimport { clc"
},
{
"path": "src/shared/logger/typeorm-logger.service.ts",
"chars": 3906,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger, LoggerOptions } from 'typeorm';\nimport {\n DEFAULT_SQL_ERR"
},
{
"path": "src/shared/logger/utils/app-root-path.util.ts",
"chars": 792,
"preview": "import { parse, resolve, join } from 'path';\nimport { existsSync } from 'fs';\n\n/**\n * 获取应用根目录\n * @returns 应用根目录\n */\nexpo"
},
{
"path": "src/shared/logger/utils/home-dir.ts",
"chars": 443,
"preview": "import * as os from 'os';\n\nexport function getHomedir(): string {\n if (process.env.MOCK_HOME_DIR) return process.env.MO"
},
{
"path": "src/shared/redis/redis.constants.ts",
"chars": 171,
"preview": "export const REDIS_CLIENT = Symbol('REDIS_CLIENT');\nexport const REDIS_MODULE_OPTIONS = Symbol('REDIS_MODULE_OPTIONS');\n"
},
{
"path": "src/shared/redis/redis.interface.ts",
"chars": 859,
"preview": "import { ModuleMetadata } from '@nestjs/common';\nimport { Redis, RedisOptions, ClusterNode, ClusterOptions } from 'iored"
},
{
"path": "src/shared/redis/redis.module.ts",
"chars": 2795,
"preview": "import {\n DynamicModule,\n Module,\n OnModuleDestroy,\n Provider,\n} from '@nestjs/common';\nimport IORedis, { Redis, Clu"
},
{
"path": "src/shared/services/redis.service.ts",
"chars": 674,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport { Cluster } from 'cluster';\nimport { Redis } from 'ioredis';"
},
{
"path": "src/shared/services/util.service.ts",
"chars": 1262,
"preview": "import { Injectable } from '@nestjs/common';\nimport { FastifyRequest } from 'fastify';\nimport { customAlphabet, nanoid }"
},
{
"path": "src/shared/shared.module.ts",
"chars": 1375,
"preview": "import { HttpModule } from '@nestjs/axios';\nimport { Global, CacheModule, Module } from '@nestjs/common';\nimport { Confi"
},
{
"path": "test/app.e2e-spec.ts",
"chars": 630,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { INestApplication } from '@nestjs/common';\nimport * as re"
},
{
"path": "test/jest-e2e.json",
"chars": 183,
"preview": "{\n \"moduleFileExtensions\": [\"js\", \"json\", \"ts\"],\n \"rootDir\": \".\",\n \"testEnvironment\": \"node\",\n \"testRegex\": \".e2e-sp"
},
{
"path": "tsconfig.build.json",
"chars": 105,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"exclude\": [\"node_modules\", \"test\", \"dist\", \"**/*spec.ts\", \"docs\"]\n}\n"
},
{
"path": "tsconfig.json",
"chars": 338,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"commonjs\",\n \"declaration\": true,\n \"removeComments\": true,\n \"emitDecorat"
}
]
About this extraction
This page contains the full source code of the hackycy/sf-nest-admin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 139 files (260.7 KB), approximately 79.9k tokens, and a symbol index with 487 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.