refactor: 华信OA后端架构搭建

This commit is contained in:
louis 2024-02-28 08:32:35 +08:00
commit c0fe67ff19
260 changed files with 27385 additions and 0 deletions

94
.cz-config.js Normal file
View File

@ -0,0 +1,94 @@
// 请使用npm run c提交代码。遵循代码提交规范
module.exports = {
types: [
{ value: 'feat', name: '功能: ✨ 新增功能', emoji: ':sparkles:' },
{ value: 'fix', name: '修复: 🐛 修复缺陷', emoji: ':bug:' },
{ value: 'docs', name: '文档: 📝 文档变更', emoji: ':memo:' },
{
value: 'style',
name: '格式: 🌈 代码格式(不影响功能,例如空格、分号等格式修正)',
emoji: ':lipstick:',
},
{
value: 'refactor',
name: '重构: 🔄 代码重构(不包括 bug 修复、功能新增)',
emoji: ':recycle:',
},
{ value: 'perf', name: '性能: 🚀 性能优化', emoji: ':zap:' },
{
value: 'test',
name: '测试: 🧪 添加疏漏测试或已有测试改动',
emoji: ':white_check_mark:',
},
{
value: 'build',
name: '构建: 📦️ 构建流程、外部依赖变更(如升级 npm 包、修改 vite 配置等)',
emoji: ':package:',
},
{
value: 'ci',
name: '集成: ⚙️ 修改 CI 配置、脚本',
emoji: ':ferris_wheel:',
},
{ value: 'revert', name: '回退: ↩️ 回滚 commit', emoji: ':rewind:' },
{
value: 'chore',
name: '其他: 🛠️ 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)',
emoji: ':hammer:',
},
],
useEmoji: true,
emojiAlign: 'center',
useAI: false,
aiNumber: 1,
themeColorCode: '',
scopes: [],
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlign: 'bottom',
customScopesAlias: 'custom',
emptyScopesAlias: 'empty',
upperCaseSubject: false,
markBreakingChangeMode: false,
breaklineNumber: 100,
breaklineChar: '|',
issuePrefixes: [
{ value: 'closed', name: 'closed: ISSUES has been processed' },
],
customIssuePrefixAlign: 'top',
emptyIssuePrefixAlias: 'skip',
customIssuePrefixAlias: 'custom',
allowCustomIssuePrefix: true,
allowEmptyIssuePrefix: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: '',
defaultIssues: '',
defaultScope: '',
defaultSubject: '',
messages: {
type: '选择一种你期望的提交类型(type):',
// scope: '选择一个更改的范围(scope) (可选):',
// used if allowCustomScopes is true
// customScope: 'Denote the SCOPE of this change:',
subject: '输入本次commit记录说明:\n',
// body: '长说明,使用"|"换行(可选)\n',
// breaking: '非兼容性说明 (可选):\n',
// footer: '关联关闭的issue例如#31, #34(可选):\n',
confirmCommit: '确定提交说明?',
},
skipQuestions: ['scope', 'body', 'breaking', 'footer'],
allowBreakingChanges: [
'fix',
'feat',
'update',
'refactor',
'perf',
'build',
'revert',
],
subjectLimit: 500, // 提交长度限制500
};

46
.dockerignore Normal file
View File

@ -0,0 +1,46 @@
# 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
# Dev data
/__data/

32
.env Normal file
View File

@ -0,0 +1,32 @@
# app
APP_NAME = Nest Admin
APP_PORT = 7001
APP_BASE_URL = http://localhost:${APP_PORT}
APP_LOCALE = zh-CN
# logger
LOGGER_LEVEL = verbose
LOGGER_MAX_FILES = 31
TZ = Asia/Shanghai
# OSS(qiniu)
OSS_ACCESSKEY=xxx
OSS_SECRETKEY=xxx
OSS_DOMAIN=https://cdn.buqiyuan.site
OSS_BUCKET=nest-admin
OSS_ZONE=Zone_z2 # Zone_as0 | Zone_na0 | Zone_z0 | Zone_z1 | Zone_z2
OSS_ACCESS_TYPE=public # or private
DB_HOST = host.docker.internal
DB_PORT = 13307
DB_DATABASE = nest_admin
DB_USERNAME = root
DB_PASSWORD = root
DB_SYNCHRONIZE = false
DB_LOGGING = ["error"]
REDIS_PORT = 6379
REDIS_HOST = host.docker.internal
REDIS_PASSWORD = 123456
REDIS_DB = 0

35
.env.development Normal file
View File

@ -0,0 +1,35 @@
# logger
LOGGER_LEVEL = debug
# security
JWT_SECRET = admin!@#123
JWT_EXPIRE = 86400 # 单位秒
REFRESH_TOKEN_SECRET = admin!@#123
REFRESH_TOKEN_EXPIRE = 2592000
# swagger
SWAGGER_ENABLE = true
SWAGGER_PATH = api-docs
SWAGGER_VERSION = 1.0
# db
DB_HOST = 127.0.0.1
DB_PORT = 13307
DB_DATABASE = nest_admin
DB_USERNAME = root
DB_PASSWORD = root
DB_SYNCHRONIZE = true
DB_LOGGING = ["error"]
# redis
REDIS_PORT = 6379
REDIS_HOST = 127.0.0.1
REDIS_PASSWORD = 123456
REDIS_DB = 0
# smtp
SMTP_HOST = smtp.163.com
SMTP_PORT = 465
SMTP_USER = nest_admin@163.com
SMTP_PASS = VIPLLOIPMETTROYU

37
.env.production Normal file
View File

@ -0,0 +1,37 @@
# logger
LOGGER_LEVEL = debug
# security
JWT_SECRET = admin!@#123
JWT_EXPIRE = 86400 # 单位秒
REFRESH_TOKEN_SECRET = admin!@#123
REFRESH_TOKEN_EXPIRE = 2592000
# swagger
SWAGGER_ENABLE = true
SWAGGER_PATH = api-docs
SWAGGER_VERSION = 1.0
# db
DB_HOST = host.docker.internal
DB_PORT = 13307
DB_DATABASE = nest_admin
DB_USERNAME = root
DB_PASSWORD = root
DB_SYNCHRONIZE = false
DB_LOGGING = ["error"]
# redis
REDIS_PORT = 6379
REDIS_HOST = host.docker.internal
REDIS_PASSWORD = 123456
REDIS_DB = 0
# smtp
SMTP_HOST = smtp.163.com
SMTP_PORT = 465
SMTP_USER = nest_admin@163.com
SMTP_PASS = VIPLLOIPMETTROYU
# 是否为演示模式(在演示模式下,会拦截除 GET 方法以外的所有请求)
IS_DEMO = false

11
.gitattributes vendored Normal file
View File

@ -0,0 +1,11 @@
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
# Automatically normalize line endings (to LF) for all text-based files.
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary

77
.gitignore vendored Normal file
View File

@ -0,0 +1,77 @@
node_modules
.DS_Store
dist
*-dist
.cache
.history
.vercel/
.turbo
.local
# local env files
#.env.development
#.env.production
.env.local
.eslintcache
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-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
.nestjs_repl_history
out
# temp data
__data
public/upload
types/env.d.ts

4
.husky/commit-msg Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run commitlint

7
.husky/pre-commit Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
#推送之前运行eslint检查
npx lint-staged
##推送之前运行单元测试检查
#npm run test:unit

6
.npmrc Normal file
View File

@ -0,0 +1,6 @@
shamefully-hoist=true
strict-peer-dependencies=false
# 使用淘宝镜像源
registry = https://registry.npmmirror.com
# registry = https://registry.npmjs.org

19
.versionrc.js Normal file
View File

@ -0,0 +1,19 @@
//发布应用 生成commit日志记录。
module.exports = {
types: [
{ type: 'feat', section: '✨ Features | 新功能' },
{ type: 'fix', section: '🐛 Bug Fixes | Bug 修复' },
{ type: 'init', section: '📦️ Init | 初始化' },
{ type: 'docs', section: '📝 Documentation | 文档' },
{ type: 'style', section: '🌈 Styles | 风格' },
{ type: 'refactor', section: '🔄 Code Refactoring | 代码重构' },
{ type: 'perf', section: '🚀 Performance Improvements | 性能优化' },
{ type: 'test', section: '🧪 Tests | 测试' },
{ type: 'revert', section: '↩️ Revert | 回退' },
{ type: 'build', section: '📦️ Build System | 打包构建' },
{ type: 'update', section: '⚙️ update | 构建/工程依赖/工具升级' },
{ type: 'tool', section: '🛠️ tool | 工具升级' },
{ type: 'ci', section: '⚙️ Continuous Integration | CI 配置' },
],
};

17
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Nest Framework",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
"autoAttachChildProcesses": true,
"restart": true,
"sourceMaps": true,
"stopOnEntry": false,
"console": "integratedTerminal"
}
]
}

41
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,41 @@
{
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}

0
CHANGELOG.md Normal file
View File

53
Dockerfile Normal file
View File

@ -0,0 +1,53 @@
# 遇到网络问题可以配置镜像加速https://gist.github.com/y0ngb1n/7e8f16af3242c7815e7ca2f0833d3ea6
# FROM 表示设置要制作的镜像基于哪个镜像FROM指令必须是整个Dockerfile的第一个指令如果指定的镜像不存在默认会自动从Docker Hub上下载。
# 指定我们的基础镜像是nodelatest表示版本是最新, 如果要求空间极致可以选择lts-alpine
# 使用 as 来为某一阶段命名
FROM node:20-slim AS base
ENV PROJECT_DIR=/nest-admin \
DB_HOST=mysql \
APP_PORT=7001 \
PNPM_HOME="/pnpm" \
PATH="$PNPM_HOME:$PATH"
RUN corepack enable \
&& yarn global add pm2
# WORKDIR指令用于设置Dockerfile中的RUN、CMD和ENTRYPOINT指令执行命令的工作目录(默认为/目录)该指令在Dockerfile文件中可以出现多次
# 如果使用相对路径则为相对于WORKDIR上一次的值
# 例如WORKDIR /dataWORKDIR logsRUN pwd最终输出的当前目录是/data/logs。
# cd 到 /nest-admin
WORKDIR $PROJECT_DIR
COPY ./ $PROJECT_DIR
RUN chmod +x ./wait-for-it.sh
# set timezone
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo 'Asia/Shanghai' > /etc/timezone
# see https://pnpm.io/docker
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build
# mirror acceleration
# RUN npm config set registry https://registry.npmmirror.com
# RUN pnpm config set registry https://registry.npmmirror.com
# RUN npm config rm proxy && npm config rm https-proxy
FROM base
COPY --from=prod-deps $PROJECT_DIR/node_modules $PROJECT_DIR/node_modules
COPY --from=build $PROJECT_DIR/dist $PROJECT_DIR/dist
# EXPOSE port
EXPOSE $APP_PORT
# 容器启动时执行的命令类似npm run start
# CMD ["pnpm", "start:prod"]
# CMD ["pm2-runtime", "ecosystem.config.js"]
ENTRYPOINT ./wait-for-it.sh $DB_HOST:$DB_PORT -- pm2-runtime ecosystem.config.js

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present buqiyuan
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.

112
README.md Normal file
View File

@ -0,0 +1,112 @@
## 环境要求
- `nodejs` `16.20.2`+
- `docker` `20.x`+ ,其中 `docker compose`版本需要 `2.17.0`+
- `mysql` `8.x`+
- 使用 [`pnpm`](https://pnpm.io/zh/) 包管理器安装项目依赖
演示环境账号密码:
| 账号 | 密码 | 权限 |
| :-------: | :----: | :--------: |
| admin | a123456 | 超级管理员 |
> 所有新建的用户初始密码都为 a123456
本地部署账号密码:
| 账号 | 密码 | 权限 |
| :-------: | :----: | :--------: |
| admin | a123456 | 超级管理员 |
## 本地开发
- 【可选】如果你是新手,还不太会搭建`mysql/redis`,你可以使用 `Docker` 启动指定服务供本地开发时使用, 例如:
```bash
# 启动MySql服务
docker compose --env-file .env --env-file .env.development run -d --service-ports mysql
# 启动Redis服务
docker compose --env-file .env --env-file .env.development run -d --service-ports redis
```
- 安装依赖
```bash
pnpm install
```
- 运行
启动成功后,通过 <http://localhost:7001/api-docs/> 访问。
```bash
pnpm dev
```
- 打包
```bash
pnpm build
```
2.使用docker运行
```bash
docker compose up -d
```
停止并删除所有容器
```bash
pnpm docker:down
# or
docker compose --env-file .env --env-file .env.production down
```
删除镜像
```bash
pnpm docker:rmi
# or
docker rmi buqiyuan/nest-admin-server:stable
```
查看实时日志输出
```bash
pnpm docker:logs
# or
docker compose --env-file .env --env-file .env.production logs -f
```
## 数据库迁移
1. 更新数据库(或初始化数据)
```bash
pnpm migration:run
```
2. 生成迁移
```bash
pnpm migration:generate
```
3. 回滚到最后一次更新
```bash
pnpm migration:revert
```
更多细节,请移步至[官方文档](https://typeorm.io/migrations)
> [!TIP]
> 如果你的`实体类`或`数据库配置`有更新,请执行`npm run build`后再进行数据库迁移相关操作。

24
commitlint.config.cjs Normal file
View File

@ -0,0 +1,24 @@
// 请使用npm run c/yarn c提交代码。
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新增功能
'fix', // 修复缺陷
'docs', // 文档变更
'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
'refactor', // 代码重构(不包括 bug 修复、功能新增)
'perf', // 性能优化
'test', // 添加疏漏测试或已有测试改动
'build', // 构建流程、外部依赖变更(如升级 npm 包、修改 webpack 配置等)
'ci', // 修改 CI 配置、脚本
'revert', // 回滚 commit
'chore', // 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)
],
],
'subject-case': [0], // subject大小写不做校验
},
};

744
deploy/sql/nest_admin.sql Normal file
View File

@ -0,0 +1,744 @@
/*
Navicat Premium Data Transfer
Source Server : nest-admin
Source Server Type : MySQL
Source Server Version : 80030 (8.0.30)
Source Host : localhost:13307
Source Schema : nest_admin
Target Server Type : MySQL
Target Server Version : 80030 (8.0.30)
File Encoding : 65001
Date: 10/02/2024 09:43:40
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_captcha_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_captcha_log`;
CREATE TABLE `sys_captcha_log` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int DEFAULT NULL,
`account` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
`code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
`provider` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_captcha_log
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_config
-- ----------------------------
DROP TABLE IF EXISTS `sys_config`;
CREATE TABLE `sys_config` (
`id` int NOT NULL AUTO_INCREMENT,
`key` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `IDX_2c363c25cf99bcaab3a7f389ba` (`key`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_config
-- ----------------------------
BEGIN;
INSERT INTO `sys_config` (`id`, `key`, `name`, `value`, `remark`, `created_at`, `updated_at`) VALUES (1, 'sys_user_initPassword', '初始密码', '123456', '创建管理员账号的初始密码', '2023-11-10 00:31:44.154921', '2023-11-10 00:31:44.161263');
INSERT INTO `sys_config` (`id`, `key`, `name`, `value`, `remark`, `created_at`, `updated_at`) VALUES (2, 'sys_api_token', 'API Token', 'nest-admin', '用于请求 @ApiToken 的控制器', '2023-11-10 00:31:44.154921', '2024-01-29 09:52:27.000000');
COMMIT;
-- ----------------------------
-- Table structure for sys_dept
-- ----------------------------
DROP TABLE IF EXISTS `sys_dept`;
CREATE TABLE `sys_dept` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
`orderNo` int DEFAULT '0',
`mpath` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT '',
`parentId` int DEFAULT NULL,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`) USING BTREE,
KEY `FK_c75280b01c49779f2323536db67` (`parentId`) USING BTREE,
CONSTRAINT `FK_c75280b01c49779f2323536db67` FOREIGN KEY (`parentId`) REFERENCES `sys_dept` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_dept
-- ----------------------------
BEGIN;
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (1, '华东分部', 1, '1.', NULL, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (2, '研发部', 1, '1.2.', 1, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (3, '市场部', 2, '1.3.', 1, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (4, '商务部', 3, '1.4.', 1, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (5, '财务部', 4, '1.5.', 1, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (6, '华南分部', 2, '6.', NULL, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (7, '西北分部', 3, '7.', NULL, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (8, '研发部', 1, '6.8.', 6, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
INSERT INTO `sys_dept` (`id`, `name`, `orderNo`, `mpath`, `parentId`, `created_at`, `updated_at`) VALUES (9, '市场部', 1, '6.9.', 6, '2023-11-10 00:31:43.996025', '2023-11-10 00:31:44.008709');
COMMIT;
-- ----------------------------
-- Table structure for sys_dict
-- ----------------------------
DROP TABLE IF EXISTS `sys_dict`;
CREATE TABLE `sys_dict` (
`id` int NOT NULL AUTO_INCREMENT,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`create_by` int NOT NULL COMMENT '创建者',
`update_by` int NOT NULL COMMENT '更新者',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`status` tinyint NOT NULL DEFAULT '1',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_d112365748f740ee260b65ce91` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Records of sys_dict
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_dict_item
-- ----------------------------
DROP TABLE IF EXISTS `sys_dict_item`;
CREATE TABLE `sys_dict_item` (
`id` int NOT NULL AUTO_INCREMENT,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`create_by` int NOT NULL COMMENT '创建者',
`update_by` int NOT NULL COMMENT '更新者',
`label` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`value` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`order` int DEFAULT NULL COMMENT '字典项排序',
`status` tinyint NOT NULL DEFAULT '1',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`type_id` int DEFAULT NULL,
`orderNo` int DEFAULT NULL COMMENT '字典项排序',
PRIMARY KEY (`id`),
KEY `FK_d68ea74fcb041c8cfd1fd659844` (`type_id`),
CONSTRAINT `FK_d68ea74fcb041c8cfd1fd659844` FOREIGN KEY (`type_id`) REFERENCES `sys_dict_type` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Records of sys_dict_item
-- ----------------------------
BEGIN;
INSERT INTO `sys_dict_item` (`id`, `created_at`, `updated_at`, `create_by`, `update_by`, `label`, `value`, `order`, `status`, `remark`, `type_id`, `orderNo`) VALUES (1, '2024-01-29 01:24:51.846135', '2024-01-29 02:23:19.000000', 1, 1, '', '1', 0, 1, '性别男', 1, 3);
INSERT INTO `sys_dict_item` (`id`, `created_at`, `updated_at`, `create_by`, `update_by`, `label`, `value`, `order`, `status`, `remark`, `type_id`, `orderNo`) VALUES (2, '2024-01-29 01:32:58.458741', '2024-01-29 01:58:20.000000', 1, 1, '', '0', 1, 1, '性别女', 1, 2);
INSERT INTO `sys_dict_item` (`id`, `created_at`, `updated_at`, `create_by`, `update_by`, `label`, `value`, `order`, `status`, `remark`, `type_id`, `orderNo`) VALUES (3, '2024-01-29 01:59:17.805394', '2024-01-29 14:37:18.000000', 1, 1, '人妖王', '3', NULL, 1, '安布里奥·伊万科夫', 1, 0);
INSERT INTO `sys_dict_item` (`id`, `created_at`, `updated_at`, `create_by`, `update_by`, `label`, `value`, `order`, `status`, `remark`, `type_id`, `orderNo`) VALUES (5, '2024-01-29 02:13:01.782466', '2024-01-29 02:13:01.782466', 1, 1, '显示', '1', NULL, 1, '显示菜单', 2, 0);
INSERT INTO `sys_dict_item` (`id`, `created_at`, `updated_at`, `create_by`, `update_by`, `label`, `value`, `order`, `status`, `remark`, `type_id`, `orderNo`) VALUES (6, '2024-01-29 02:13:31.134721', '2024-01-29 02:13:31.134721', 1, 1, '隐藏', '0', NULL, 1, '隐藏菜单', 2, 0);
COMMIT;
-- ----------------------------
-- Table structure for sys_dict_type
-- ----------------------------
DROP TABLE IF EXISTS `sys_dict_type`;
CREATE TABLE `sys_dict_type` (
`id` int NOT NULL AUTO_INCREMENT,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`create_by` int NOT NULL COMMENT '创建者',
`update_by` int NOT NULL COMMENT '更新者',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`status` tinyint NOT NULL DEFAULT '1',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_74d0045ff7fab9f67adc0b1bda` (`code`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ----------------------------
-- Records of sys_dict_type
-- ----------------------------
BEGIN;
INSERT INTO `sys_dict_type` (`id`, `created_at`, `updated_at`, `create_by`, `update_by`, `name`, `status`, `remark`, `code`) VALUES (1, '2024-01-28 08:19:12.777447', '2024-02-08 13:05:10.000000', 1, 1, '性别', 1, '性别单选', 'sys_user_gender');
INSERT INTO `sys_dict_type` (`id`, `created_at`, `updated_at`, `create_by`, `update_by`, `name`, `status`, `remark`, `code`) VALUES (2, '2024-01-28 08:38:41.235185', '2024-01-29 02:11:33.000000', 1, 1, '菜单显示状态', 1, '菜单显示状态', 'sys_show_hide');
COMMIT;
-- ----------------------------
-- Table structure for sys_login_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_login_log`;
CREATE TABLE `sys_login_log` (
`id` int NOT NULL AUTO_INCREMENT,
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`ua` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`provider` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`user_id` int DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `FK_3029712e0df6a28edaee46fd470` (`user_id`),
CONSTRAINT `FK_3029712e0df6a28edaee46fd470` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_login_log
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` int NOT NULL AUTO_INCREMENT,
`parent_id` int DEFAULT NULL,
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`permission` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`type` tinyint NOT NULL DEFAULT '0',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '',
`order_no` int DEFAULT '0',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`keep_alive` tinyint NOT NULL DEFAULT '1',
`show` tinyint NOT NULL DEFAULT '1',
`status` tinyint NOT NULL DEFAULT '1',
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`is_ext` tinyint NOT NULL DEFAULT '0',
`ext_open_mode` tinyint NOT NULL DEFAULT '1',
`active_menu` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=128 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
BEGIN;
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (1, NULL, '/system', '系统管理', '', 0, 'ant-design:setting-outlined', 254, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-27 18:53:46.668745', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (2, 1, '/system/user', '用户管理', 'system:user:list', 1, 'ant-design:user-outlined', 0, 'system/user/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:10:30.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (3, 1, '/system/role', '角色管理', 'system:role:list', 1, 'ep:user', 1, 'system/role/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:11:02.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (4, 1, '/system/menu', '菜单管理', 'system:menu:list', 1, 'ep:menu', 2, 'system/menu/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:11:18.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (5, 1, '/system/monitor', '系统监控', '', 0, 'ep:monitor', 5, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-27 18:53:44.567023', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (6, 5, '/system/monitor/online', '在线用户', 'system:online:list', 1, '', 0, 'system/monitor/online/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-15 22:13:59.519267', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (7, 5, '/sys/monitor/login-log', '登录日志', 'system:log:login:list', 1, '', 0, 'system/monitor/log/login/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-15 22:14:02.610719', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (8, 5, '/system/monitor/serve', '服务监控', 'system:serve:stat', 1, '', 4, 'system/monitor/serve/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-15 22:14:05.606355', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (9, 1, '/system/schedule', '任务调度', '', 0, 'ant-design:schedule-filled', 6, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-27 18:53:52.967983', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (10, 9, '/system/task', '任务管理', '', 1, '', 0, 'system/schedule/task/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:14:39.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (11, 9, '/system/task/log', '任务日志', 'system:task:list', 1, '', 0, 'system/schedule/log/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:15:01.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (12, NULL, '/document', '文档', '', 0, 'ion:tv-outline', 2, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-27 18:53:42.514264', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (14, 12, 'https://www.typeorm.org/', 'Typeorm中文文档(外链)', NULL, 1, '', 3, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-30 18:39:53.000000', 1, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (15, 12, 'https://docs.nestjs.cn/', 'Nest.js中文文档(内嵌)', '', 1, '', 4, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-30 18:40:43.000000', 1, 2, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (20, 2, NULL, '新增', 'system:user:create', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (21, 2, '', '删除', 'system:user:delete', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (22, 2, '', '更新', 'system:user:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (23, 2, '', '查询', 'system:user:read', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (24, 3, '', '新增', 'system:role:create', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (25, 3, '', '删除', 'system:role:delete', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (26, 3, '', '修改', 'system:role:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (27, 3, '', '查询', 'system:role:read', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (28, 4, NULL, '新增', 'system:menu:create', 2, NULL, 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (29, 4, NULL, '删除', 'system:menu:delete', 2, NULL, 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (30, 4, '', '修改', 'system:menu:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (31, 4, NULL, '查询', 'system:menu:read', 2, NULL, 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (32, 6, '', '下线', 'system:online:kick', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (34, 10, '', '新增', 'system:task:create', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (35, 10, '', '删除', 'system:task:delete', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (36, 10, '', '执行一次', 'system:task:once', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (37, 10, '', '查询', 'system:task:read', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (38, 10, '', '运行', 'system:task:start', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (39, 10, '', '暂停', 'system:task:stop', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (40, 10, '', '更新', 'system:task:update', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (41, 7, '', '查询登录日志', 'system:log:login:list', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (42, 7, '', '查询任务日志', 'system:log:task:list', 2, '', 0, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (43, NULL, '/about', '关于', '', 1, 'ant-design:info-circle-outlined', 260, 'account/about', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-02-10 09:35:41.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (48, NULL, '/tool', '系统工具', NULL, 0, 'ant-design:tool-outlined', 254, '', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-27 18:53:28.327223', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (49, 48, '/tool/email', '邮件工具', 'system:tools:email', 1, 'ant-design:send-outlined', 1, 'tool/email/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-25 00:59:07.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (50, 49, NULL, '发送邮件', 'tools:email:send', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (51, 48, '/tool/storage', '存储管理', 'tool:storage:list', 1, 'ant-design:appstore-outlined', 2, 'tool/storage/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-25 00:59:17.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (52, 51, NULL, '文件上传', 'upload:upload', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-25 01:04:08.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (53, 51, NULL, '文件删除', 'tool:storage:delete', 2, '', 2, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-25 00:56:01.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (54, 2, NULL, '修改密码', 'system:user:password', 2, '', 5, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (56, 1, '/system/dict-type', '字典管理', 'system:dict-type:list', 1, 'ant-design:book-outlined', 4, 'system/dict-type/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:12.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (57, 56, NULL, '新增', 'system:dict-type:create', 2, '', 1, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:20.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (58, 56, NULL, '更新', 'system:dict-type:update', 2, '', 2, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:26.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (59, 56, NULL, '删除', 'system:dict-type:delete', 2, '', 3, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:42.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (60, 56, NULL, '查询', 'system:dict-type:info', 2, '', 4, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-28 09:07:36.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (61, 1, '/system/dept', '部门管理', 'system:dept:list', 1, 'ant-design:deployment-unit-outlined', 3, 'system/dept/index', 1, 1, 1, '2023-11-10 00:31:44.023393', '2024-01-17 03:11:55.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (62, 61, NULL, '新增', 'system:dept:create', 2, '', 1, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (63, 61, NULL, '更新', 'system:dept:update', 2, '', 2, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (64, 61, NULL, '删除', 'system:dept:delete', 2, '', 3, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (65, 61, NULL, '查询', 'system:dept:read', 2, '', 4, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (68, 5, '/health', '健康检查', '', 1, '', 4, '', 1, 0, 1, '2023-11-10 00:31:44.023393', '2024-01-27 18:53:33.352155', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (69, 68, NULL, '网络', 'app:health:network', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (70, 68, NULL, '数据库', 'app:health: database', 2, '', 0, NULL, 1, 1, 1, '2023-11-10 00:31:44.023393', '2023-11-10 00:31:44.034474', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (86, 1, '/param-config', '参数配置', 'system:param-config:list', 1, 'ep:edit', 255, 'system/param-config/index', 0, 1, 1, '2024-01-10 17:34:52.569663', '2024-01-19 02:11:27.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (87, 86, NULL, '查询', 'system:param-config:read', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:39:20.983241', '2024-01-10 17:39:20.983241', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (88, 86, NULL, '新增', 'system:param-config:create', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:39:57.543510', '2024-01-10 17:39:57.543510', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (89, 86, NULL, '更新', 'system:param-config:update', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:40:27.355944', '2024-01-10 17:40:27.355944', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (92, 86, NULL, '删除', 'system:param-config:delete', 2, '', 255, NULL, 1, 1, 1, '2024-01-10 17:57:32.059887', '2024-01-10 17:57:32.059887', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (107, 1, 'system/dict-item/:id', '字典项管理', 'system:dict-item:list', 1, 'ant-design:facebook-outlined', 255, 'system/dict-item/index', 0, 0, 1, '2024-01-28 09:21:17.409532', '2024-01-30 13:09:47.000000', 0, 1, '字典管理');
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (108, 107, NULL, '新增', 'system:dict-item:create', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:22:39.401758', '2024-01-28 22:38:36.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (109, 107, NULL, '更新', 'system:dict-item:update', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:26:43.911886', '2024-01-28 09:26:43.911886', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (110, 107, NULL, '删除', 'system:dict-item:delete', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:27:28.535225', '2024-01-28 09:27:28.535225', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (111, 107, NULL, '查询', 'system:dict-item:info', 2, '', 255, NULL, 1, 1, 1, '2024-01-28 09:27:43.894820', '2024-01-28 09:27:43.894820', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (112, 12, 'https://antdv.com/components/overview-cn', 'antdv文档(内嵌)', NULL, 1, '', 255, NULL, 1, 1, 1, '2024-01-29 09:23:08.407723', '2024-01-30 18:41:19.000000', 1, 2, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (115, NULL, 'netdisk', '网盘管理', NULL, 0, 'ant-design:cloud-server-outlined', 255, NULL, 1, 1, 1, '2024-02-10 08:00:02.394616', '2024-02-10 09:35:34.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (116, 115, 'manage', '文件管理', 'netdisk:manage:list', 1, '', 252, 'netdisk/manage', 0, 1, 1, '2024-02-10 08:03:49.837348', '2024-02-10 09:34:41.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (117, 116, NULL, '创建文件或文件夹', 'netdisk:manage:create', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:40:22.317257', '2024-02-10 08:40:22.317257', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (118, 116, NULL, '查看文件', 'netdisk:manage:read', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:41:22.008015', '2024-02-10 08:41:22.008015', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (119, 116, NULL, '更新', 'netdisk:manage:update', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:41:50.691643', '2024-02-10 08:41:50.691643', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (120, 116, NULL, '删除', 'netdisk:manage:delete', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:42:09.480601', '2024-02-10 08:42:09.480601', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (121, 116, NULL, '获取文件上传token', 'netdisk:manage:token', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:42:57.688104', '2024-02-10 08:42:57.688104', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (122, 116, NULL, '添加文件备注', 'netdisk:manage:mark', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:43:40.117321', '2024-02-10 08:43:40.117321', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (123, 116, NULL, '下载文件', 'netdisk:manage:download', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:44:01.338984', '2024-02-10 08:44:01.338984', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (124, 116, NULL, '重命名文件或文件夹', 'netdisk:manage:rename', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:44:27.233379', '2024-02-10 08:45:36.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (125, 116, NULL, '复制文件或文件夹', 'netdisk:manage:copy', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:44:44.725391', '2024-02-10 08:45:48.000000', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (126, 116, NULL, '剪切文件或文件夹', 'netdisk:manage:cut', 2, '', 255, NULL, 1, 1, 1, '2024-02-10 08:45:21.660511', '2024-02-10 08:45:21.660511', 0, 1, NULL);
INSERT INTO `sys_menu` (`id`, `parent_id`, `path`, `name`, `permission`, `type`, `icon`, `order_no`, `component`, `keep_alive`, `show`, `status`, `created_at`, `updated_at`, `is_ext`, `ext_open_mode`, `active_menu`) VALUES (127, 115, 'overview', '网盘概览', 'netdisk:overview:desc', 1, '', 254, 'netdisk/overview', 0, 1, 1, '2024-02-10 09:32:56.981190', '2024-02-10 09:34:18.000000', 0, 1, NULL);
COMMIT;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int NOT NULL AUTO_INCREMENT,
`value` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`status` tinyint DEFAULT '1',
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`default` tinyint DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `IDX_223de54d6badbe43a5490450c3` (`name`) USING BTREE,
UNIQUE KEY `IDX_05edc0a51f41bb16b7d8137da9` (`value`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
BEGIN;
INSERT INTO `sys_role` (`id`, `value`, `name`, `remark`, `status`, `created_at`, `updated_at`, `default`) VALUES (1, 'admin', '管理员', '超级管理员', 1, '2023-11-10 00:31:44.058463', '2024-01-28 21:08:39.000000', NULL);
INSERT INTO `sys_role` (`id`, `value`, `name`, `remark`, `status`, `created_at`, `updated_at`, `default`) VALUES (2, 'user', '用户', '', 1, '2023-11-10 00:31:44.058463', '2024-01-30 18:44:45.000000', 1);
INSERT INTO `sys_role` (`id`, `value`, `name`, `remark`, `status`, `created_at`, `updated_at`, `default`) VALUES (9, 'test', '测试', NULL, 1, '2024-01-23 22:46:52.408827', '2024-01-30 01:04:52.000000', NULL);
COMMIT;
-- ----------------------------
-- Table structure for sys_role_menus
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menus`;
CREATE TABLE `sys_role_menus` (
`role_id` int NOT NULL,
`menu_id` int NOT NULL,
PRIMARY KEY (`role_id`,`menu_id`),
KEY `IDX_35ce749b04d57e226d059e0f63` (`role_id`),
KEY `IDX_2b95fdc95b329d66c18f5baed6` (`menu_id`),
CONSTRAINT `FK_2b95fdc95b329d66c18f5baed6d` FOREIGN KEY (`menu_id`) REFERENCES `sys_menu` (`id`) ON DELETE CASCADE,
CONSTRAINT `FK_35ce749b04d57e226d059e0f633` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of sys_role_menus
-- ----------------------------
BEGIN;
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 1);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 2);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 3);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 4);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 5);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 6);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 7);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 8);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 9);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 10);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 11);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 12);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 14);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 15);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 20);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 21);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 22);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 23);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 24);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 25);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 26);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 27);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 28);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 29);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 30);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 31);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 32);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 34);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 35);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 36);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 37);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 38);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 39);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 40);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 41);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 42);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 43);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 48);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 49);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 50);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 51);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 52);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 53);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 54);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 56);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 57);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 58);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 59);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 60);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 61);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 62);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 63);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 64);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 65);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 68);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 69);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 70);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 86);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 87);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 88);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 89);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 92);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 107);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 108);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 109);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 110);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (1, 111);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 1);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 5);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 6);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 7);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 8);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 9);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 10);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 11);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 12);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 14);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 15);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 32);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 34);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 35);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 36);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 37);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 38);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 39);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 40);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 41);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 42);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 43);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 48);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 49);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 50);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 51);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 52);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 53);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 56);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 57);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 58);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 59);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 60);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 68);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 69);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 70);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 86);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 87);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 88);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 89);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 92);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 107);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 108);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 109);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 110);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 111);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (2, 112);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 1);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 2);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 3);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 4);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 5);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 6);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 7);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 8);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 9);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 10);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 11);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 20);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 21);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 22);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 23);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 24);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 25);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 26);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 27);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 28);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 29);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 30);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 31);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 32);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 34);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 35);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 36);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 37);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 38);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 39);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 40);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 41);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 42);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 54);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 56);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 57);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 58);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 59);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 60);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 61);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 62);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 63);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 64);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 65);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 68);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 69);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 70);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 86);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 87);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 88);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 89);
INSERT INTO `sys_role_menus` (`role_id`, `menu_id`) VALUES (9, 92);
COMMIT;
-- ----------------------------
-- Table structure for sys_task
-- ----------------------------
DROP TABLE IF EXISTS `sys_task`;
CREATE TABLE `sys_task` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`service` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`type` tinyint NOT NULL DEFAULT '0',
`status` tinyint NOT NULL DEFAULT '1',
`start_time` datetime DEFAULT NULL,
`end_time` datetime DEFAULT NULL,
`limit` int DEFAULT '0',
`cron` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`every` int DEFAULT NULL,
`data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
`job_opts` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `IDX_ef8e5ab5ef2fe0ddb1428439ef` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_task
-- ----------------------------
BEGIN;
INSERT INTO `sys_task` (`id`, `name`, `service`, `type`, `status`, `start_time`, `end_time`, `limit`, `cron`, `every`, `data`, `job_opts`, `remark`, `created_at`, `updated_at`) VALUES (2, '定时清空登录日志', 'LogClearJob.clearLoginLog', 0, 1, NULL, NULL, 0, '0 0 3 ? * 1', 0, '', '{\"count\":1,\"key\":\"__default__:2:::0 0 3 ? * 1\",\"cron\":\"0 0 3 ? * 1\",\"jobId\":2}', '定时清空登录日志', '2023-11-10 00:31:44.197779', '2024-02-10 09:43:14.000000');
INSERT INTO `sys_task` (`id`, `name`, `service`, `type`, `status`, `start_time`, `end_time`, `limit`, `cron`, `every`, `data`, `job_opts`, `remark`, `created_at`, `updated_at`) VALUES (3, '定时清空任务日志', 'LogClearJob.clearTaskLog', 0, 1, NULL, NULL, 0, '0 0 3 ? * 1', 0, '', '{\"count\":1,\"key\":\"__default__:3:::0 0 3 ? * 1\",\"cron\":\"0 0 3 ? * 1\",\"jobId\":3}', '定时清空任务日志', '2023-11-10 00:31:44.197779', '2024-02-10 09:43:14.000000');
INSERT INTO `sys_task` (`id`, `name`, `service`, `type`, `status`, `start_time`, `end_time`, `limit`, `cron`, `every`, `data`, `job_opts`, `remark`, `created_at`, `updated_at`) VALUES (4, '访问百度首页', 'HttpRequestJob.handle', 0, 0, NULL, NULL, 1, '* * * * * ?', NULL, '{\"url\":\"https://www.baidu.com\",\"method\":\"get\"}', NULL, '访问百度首页', '2023-11-10 00:31:44.197779', '2023-11-10 00:31:44.206935');
INSERT INTO `sys_task` (`id`, `name`, `service`, `type`, `status`, `start_time`, `end_time`, `limit`, `cron`, `every`, `data`, `job_opts`, `remark`, `created_at`, `updated_at`) VALUES (5, '发送邮箱', 'EmailJob.send', 0, 0, NULL, NULL, -1, '0 0 0 1 * ?', NULL, '{\"subject\":\"这是标题\",\"to\":\"zeyu57@163.com\",\"content\":\"这是正文\"}', NULL, '每月发送邮箱', '2023-11-10 00:31:44.197779', '2023-11-10 00:31:44.206935');
COMMIT;
-- ----------------------------
-- Table structure for sys_task_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_task_log`;
CREATE TABLE `sys_task_log` (
`id` int NOT NULL AUTO_INCREMENT,
`task_id` int DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT '0',
`detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,
`consume_time` int DEFAULT '0',
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`) USING BTREE,
KEY `FK_f4d9c36052fdb188ff5c089454b` (`task_id`),
CONSTRAINT `FK_f4d9c36052fdb188ff5c089454b` FOREIGN KEY (`task_id`) REFERENCES `sys_task` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_task_log
-- ----------------------------
BEGIN;
INSERT INTO `sys_task_log` (`id`, `task_id`, `status`, `detail`, `consume_time`, `created_at`, `updated_at`) VALUES (1, 3, 1, NULL, 0, '2024-02-05 03:06:22.037448', '2024-02-05 03:06:22.037448');
INSERT INTO `sys_task_log` (`id`, `task_id`, `status`, `detail`, `consume_time`, `created_at`, `updated_at`) VALUES (2, 2, 1, NULL, 0, '2024-02-10 09:42:21.738712', '2024-02-10 09:42:21.738712');
COMMIT;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`psalt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`status` tinyint DEFAULT '1',
`qq` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`dept_id` int DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `IDX_9e7164b2f1ea1348bc0eb0a7da` (`username`) USING BTREE,
KEY `FK_96bde34263e2ae3b46f011124ac` (`dept_id`),
CONSTRAINT `FK_96bde34263e2ae3b46f011124ac` FOREIGN KEY (`dept_id`) REFERENCES `sys_dept` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` (`id`, `username`, `password`, `avatar`, `email`, `phone`, `remark`, `psalt`, `status`, `qq`, `created_at`, `updated_at`, `nickname`, `dept_id`) VALUES (1, 'admin', 'a11571e778ee85e82caae2d980952546', 'https://thirdqq.qlogo.cn/g?b=qq&s=100&nk=1743369777', '1743369777@qq.com', '10086', '管理员', 'xQYCspvFb8cAW6GG1pOoUGTLqsuUSO3d', 1, '1743369777', '2023-11-10 00:31:44.104382', '2024-01-29 09:49:43.000000', 'bqy', 1);
INSERT INTO `sys_user` (`id`, `username`, `password`, `avatar`, `email`, `phone`, `remark`, `psalt`, `status`, `qq`, `created_at`, `updated_at`, `nickname`, `dept_id`) VALUES (2, 'user', 'dbd89546dec743f82bb9073d6ac39361', 'https://thirdqq.qlogo.cn/g?b=qq&s=100&nk=1743369777', 'luffy@qq.com', '10010', '王路飞', 'qlovDV7pL5dPYPI3QgFFo1HH74nP6sJe', 1, '1743369777', '2023-11-10 00:31:44.104382', '2024-01-29 09:49:57.000000', 'luffy', 8);
INSERT INTO `sys_user` (`id`, `username`, `password`, `avatar`, `email`, `phone`, `remark`, `psalt`, `status`, `qq`, `created_at`, `updated_at`, `nickname`, `dept_id`) VALUES (8, 'developer', 'f03fa2a99595127b9a39587421d471f6', '/upload/cfd0d14459bc1a47-202402032141838.jpeg', 'nami@qq.com', '10000', '小贼猫', 'NbGM1z9Vhgo7f4dd2I7JGaGP12RidZdE', 1, '1743369777', '2023-11-10 00:31:44.104382', '2024-02-03 21:41:18.000000', '娜美', 7);
COMMIT;
-- ----------------------------
-- Table structure for sys_user_roles
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_roles`;
CREATE TABLE `sys_user_roles` (
`user_id` int NOT NULL,
`role_id` int NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `IDX_96311d970191a044ec048011f4` (`user_id`),
KEY `IDX_6d61c5b3f76a3419d93a421669` (`role_id`),
CONSTRAINT `FK_6d61c5b3f76a3419d93a4216695` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`),
CONSTRAINT `FK_96311d970191a044ec048011f44` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of sys_user_roles
-- ----------------------------
BEGIN;
INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (1, 1);
INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (2, 2);
INSERT INTO `sys_user_roles` (`user_id`, `role_id`) VALUES (8, 2);
COMMIT;
-- ----------------------------
-- Table structure for todo
-- ----------------------------
DROP TABLE IF EXISTS `todo`;
CREATE TABLE `todo` (
`id` int NOT NULL AUTO_INCREMENT,
`value` varchar(255) NOT NULL,
`user_id` int DEFAULT NULL,
`status` tinyint NOT NULL DEFAULT '0',
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`),
KEY `FK_9cb7989853c4cb7fe427db4b260` (`user_id`),
CONSTRAINT `FK_9cb7989853c4cb7fe427db4b260` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of todo
-- ----------------------------
BEGIN;
INSERT INTO `todo` (`id`, `value`, `user_id`, `status`, `created_at`, `updated_at`) VALUES (1, 'nest.js', NULL, 0, '2023-11-10 00:31:44.139730', '2023-11-10 00:31:44.147629');
COMMIT;
-- ----------------------------
-- Table structure for tool_storage
-- ----------------------------
DROP TABLE IF EXISTS `tool_storage`;
CREATE TABLE `tool_storage` (
`id` int NOT NULL AUTO_INCREMENT,
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件名',
`fileName` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '真实文件名',
`ext_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`size` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`user_id` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=79 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of tool_storage
-- ----------------------------
BEGIN;
INSERT INTO `tool_storage` (`id`, `created_at`, `updated_at`, `name`, `fileName`, `ext_name`, `path`, `type`, `size`, `user_id`) VALUES (78, '2024-02-03 21:41:16.851178', '2024-02-03 21:41:16.851178', 'cfd0d14459bc1a47-202402032141838.jpeg', 'cfd0d14459bc1a47.jpeg', 'jpeg', '/upload/cfd0d14459bc1a47-202402032141838.jpeg', '图片', '33.92 KB', 1);
COMMIT;
-- ----------------------------
-- Table structure for user_access_tokens
-- ----------------------------
DROP TABLE IF EXISTS `user_access_tokens`;
CREATE TABLE `user_access_tokens` (
`id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`value` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`expired_at` datetime NOT NULL COMMENT '令牌过期时间',
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '令牌创建时间',
`user_id` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_e9d9d0c303432e4e5e48c1c3e90` (`user_id`),
CONSTRAINT `FK_e9d9d0c303432e4e5e48c1c3e90` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of user_access_tokens
-- ----------------------------
BEGIN;
INSERT INTO `user_access_tokens` (`id`, `value`, `expired_at`, `created_at`, `user_id`) VALUES ('09cf7b0a-62e0-45ee-96b0-e31de32361e0', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MDc1MDkxNTd9.0gtKlcxrxQ-TarEai2lsBnfMc852ZDYHeSjjhpo5Fn8', '2024-02-11 04:05:58', '2024-02-10 04:05:57.696509', 1);
INSERT INTO `user_access_tokens` (`id`, `value`, `expired_at`, `created_at`, `user_id`) VALUES ('3f7dffae-db1f-47dc-9677-5c956c3de39e', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MDczMTEzMDJ9.D5Qpht1RquKor8WtgfGAcCp8LwG7z3FZhIwbyQzhDmE', '2024-02-08 21:08:22', '2024-02-07 21:08:22.130066', 1);
INSERT INTO `user_access_tokens` (`id`, `value`, `expired_at`, `created_at`, `user_id`) VALUES ('40342c3e-194c-42eb-adee-189389839195', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MDczNzIxNjF9.tRQOxhB-01Pcut5MXm4L5D1OrbMJfS4LfUys0XB4kWs', '2024-02-09 14:02:41', '2024-02-08 14:02:41.081164', 1);
INSERT INTO `user_access_tokens` (`id`, `value`, `expired_at`, `created_at`, `user_id`) VALUES ('9d1ba8e9-dffc-4b15-b21f-4a90f196e39c', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MDc1Mjc5MDV9.7LeiS3LBBdiAc7YrULWpmnI1oNSvR79K-qjEOlBYOnI', '2024-02-11 09:18:26', '2024-02-10 09:18:25.656695', 1);
INSERT INTO `user_access_tokens` (`id`, `value`, `expired_at`, `created_at`, `user_id`) VALUES ('edbed8fb-bfc7-4fc7-a012-e9fca8ef93fb', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsInB2IjoxLCJyb2xlcyI6WyJhZG1pbiJdLCJpYXQiOjE3MDczNzIxMjd9.VRuJHGca2IPrdfTyW09wfhht4x8JX207pKG-0aZyF60', '2024-02-09 14:02:07', '2024-02-08 14:02:07.390658', 1);
COMMIT;
-- ----------------------------
-- Table structure for user_refresh_tokens
-- ----------------------------
DROP TABLE IF EXISTS `user_refresh_tokens`;
CREATE TABLE `user_refresh_tokens` (
`id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`value` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`expired_at` datetime NOT NULL COMMENT '令牌过期时间',
`created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) COMMENT '令牌创建时间',
`accessTokenId` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `REL_1dfd080c2abf42198691b60ae3` (`accessTokenId`),
CONSTRAINT `FK_1dfd080c2abf42198691b60ae39` FOREIGN KEY (`accessTokenId`) REFERENCES `user_access_tokens` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ----------------------------
-- Records of user_refresh_tokens
-- ----------------------------
BEGIN;
INSERT INTO `user_refresh_tokens` (`id`, `value`, `expired_at`, `created_at`, `accessTokenId`) VALUES ('202d0969-6721-4f6f-bf34-f0d1931d4d01', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiRTRpOXVYei1TdldjdWRnclFXVmFXIiwiaWF0IjoxNzA3MzcyMTYxfQ.NOQufR5EAPE2uZoyenmAj9H7S7qo4d6W1aW2ojDxZQc', '2024-03-09 14:02:41', '2024-02-08 14:02:41.091492', '40342c3e-194c-42eb-adee-189389839195');
INSERT INTO `user_refresh_tokens` (`id`, `value`, `expired_at`, `created_at`, `accessTokenId`) VALUES ('461f9b7c-e500-4762-a6d9-f9ea47163064', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoicXJvTWNYMnhNRW5uRmZGWkQtaUx0IiwiaWF0IjoxNzA3MzExMzAyfQ.dFIWCePZnn2z2Qv1D5PKBKXUwVDI0Gp091MIOi9jiIo', '2024-03-08 21:08:22', '2024-02-07 21:08:22.145464', '3f7dffae-db1f-47dc-9677-5c956c3de39e');
INSERT INTO `user_refresh_tokens` (`id`, `value`, `expired_at`, `created_at`, `accessTokenId`) VALUES ('b375e623-2d82-48f0-9b7a-9058e3850cc6', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoicDhUMzdGNFFaUDJHLU5yNGVha21wIiwiaWF0IjoxNzA3MzcyMTI3fQ.fn3It6RKIxXlKmqixg0BMmY_YsQmAxtetueqW-0y1IM', '2024-03-09 14:02:07', '2024-02-08 14:02:07.410008', 'edbed8fb-bfc7-4fc7-a012-e9fca8ef93fb');
INSERT INTO `user_refresh_tokens` (`id`, `value`, `expired_at`, `created_at`, `accessTokenId`) VALUES ('e620ccc1-9e40-4387-9f21-f0722e535a63', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiNE5WdmFIc2hWaU05ZFh0QnVBaHNsIiwiaWF0IjoxNzA3NTI3OTA1fQ.zzyGX0mOJe6KWpTzIi7We9d9c0MRuDeGC86DMB0Vubs', '2024-03-11 09:18:26', '2024-02-10 09:18:25.664251', '9d1ba8e9-dffc-4b15-b21f-4a90f196e39c');
INSERT INTO `user_refresh_tokens` (`id`, `value`, `expired_at`, `created_at`, `accessTokenId`) VALUES ('f9a003e8-91b7-41ee-979e-e39cca3534ec', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1dWlkIjoiWGJQdl9SVjFtUl80N0o0TGF0QlV5IiwiaWF0IjoxNzA3NTA5MTU3fQ.oEVdWSigTpAQY7F8MlwBnedldH0sJT1YF1Mt0ZUbIw4', '2024-03-11 04:05:58', '2024-02-10 04:05:57.706763', '09cf7b0a-62e0-45ee-96b0-e31de32361e0');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

54
deploy/web/default.conf Normal file
View File

@ -0,0 +1,54 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
absolute_redirect off; #取消绝对路径的重定向
sendfile on;
default_type application/octet-stream;
gzip on;
gzip_http_version 1.1;
gzip_disable "MSIE [1-6]\.";
gzip_min_length 256;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
gzip_comp_level 9;
root /usr/share/nginx/html;
location / {
# same docker config
root /usr/share/nginx/html;
index index.html;
# support history mode
try_files $uri $uri/ /index.html;
}
# 后端服务
location ^~ /api/ {
proxy_pass http://nest-admin-server:7001/; # 转发规则
proxy_set_header Host $proxy_host; # 修改转发请求头,让目标应用可以受到真实的请求
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# websocket服务
location ^~ /ws-api/ {
proxy_pass http://nest-admin-server:7002/;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}

64
docker-compose.yml Normal file
View File

@ -0,0 +1,64 @@
version: '3'
services:
mysql:
image: mysql:latest
container_name: nest-admin-mysql
restart: always
env_file:
- .env
- .env.production
environment:
- MYSQL_HOST=${DB_HOST}
- MYSQL_PORT=${DB_PORT}
- MYSQL_DATABASE=${DB_DATABASE}
- MYSQL_USERNAME=${DB_USERNAME}
- MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
ports:
- '${DB_PORT}:3306'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci #设置utf8字符集
volumes:
- ./__data/mysql/:/var/lib/mysql/ # ./__data/mysql/ 路径可以替换成自己的路径
- ./deploy/sql/:/docker-entrypoint-initdb.d/ # 初始化的脚本,若 ./__data/mysql/ 文件夹存在数据,则不会执行初始化脚本
networks:
- nest_admin_net
redis:
image: redis:alpine
container_name: nest-admin-redis
restart: always
env_file:
- .env
- .env.production
ports:
- '${REDIS_PORT}:6379'
command: >
--requirepass ${REDIS_PASSWORD}
networks:
- nest_admin_net
nest-admin-server:
# build: 从当前路径构建镜像
build:
context: .
dockerfile: Dockerfile
container_name: nest-admin-server
restart: always
env_file:
- .env
- .env.production
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT}:${APP_PORT}'
# 当前服务启动之前先要把depends_on指定的服务启动起来才行
depends_on:
- mysql
- redis
networks:
- nest_admin_net
networks:
nest_admin_net:
name: nest_admin_net

22
ecosystem.config.js Normal file
View File

@ -0,0 +1,22 @@
const { cpus } = require('os')
const cpuLen = cpus().length
module.exports = {
apps: [
{
name: 'nest-admin',
script: './dist/main.js',
autorestart: true,
exec_mode: 'cluster',
watch: false,
instances: cpuLen,
max_memory_restart: '520M',
args: '',
env: {
NODE_ENV: 'production',
PORT: process.env.APP_PORT,
},
},
],
}

36
eslint.config.js Normal file
View File

@ -0,0 +1,36 @@
const antfu = require('@antfu/eslint-config').default
module.exports = antfu({
stylistic: {
indent: 2,
quotes: 'single',
},
typescript: true,
}, {
rules: {
'no-console': 'off',
'unused-imports/no-unused-vars': 'off',
'unused-imports/no-unused-imports': 2,
'ts/consistent-type-imports': 'off',
'node/prefer-global/process': 'off',
'node/prefer-global/buffer': 'off',
'import/order': [
2,
{
'pathGroups': [
{
pattern: '~/**',
group: 'external',
position: 'after',
},
],
'alphabetize': { order: 'asc', caseInsensitive: false },
'newlines-between': 'always-and-inside-groups',
'warnOnUnassignedImports': true,
},
],
},
})

17
nest-cli.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"assets": [
{ "include": "assets/**/*", "outDir": "dist", "watchAssets": true }
],
"plugins": [{
"name": "@nestjs/swagger",
"options": {
"introspectComments": true
}
}]
}
}

174
package.json Normal file
View File

@ -0,0 +1,174 @@
{
"name": "nest-admin",
"version": "2.0.0",
"private": true,
"packageManager": "pnpm@8.10.2",
"license": "MIT",
"engines": {
"node": ">=18",
"pnpm": ">=8.1.0"
},
"scripts": {
"postinstall": "npm run gen-env-types",
"prebuild": "rimraf dist",
"build": "nest build",
"dev": "npm run start",
"dev:debug": "npm run start:debug",
"repl": "npm run start -- --entryFile repl",
"bundle": "rimraf out && npm run build && ncc build dist/main.js -o out -m -t && chmod +x out/index.js",
"start": "cross-env NODE_ENV=development nest start -w --path tsconfig.json",
"start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
"start:prod": "cross-env NODE_ENV=production node dist/main",
"prod": "cross-env NODE_ENV=production pm2-runtime start ecosystem.config.js",
"prod:pm2": "cross-env NODE_ENV=production pm2 restart ecosystem.config.js",
"prod:stop": "pm2 stop ecosystem.config.js",
"prod:debug": "cross-env NODE_ENV=production nest start --debug --watch",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"doc": "compodoc -p tsconfig.json -s",
"gen-env-types": "npx tsx scripts/genEnvTypes.ts",
"typeorm": "NODE_ENV=development typeorm-ts-node-esm -d ./dist/config/database.config.js",
"migration:create": "npm run typeorm migration:create ./src/migrations/initData",
"migration:generate": "npm run typeorm migration:generate ./src/migrations/update-table_$(echo $npm_package_version | sed 's/\\./_/g')",
"migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert",
"cleanlog": "rimraf logs",
"docker:build:dev": "docker compose --env-file .env --env-file .env.development up --build",
"docker:build": "docker compose --env-file .env --env-file .env.production up --build",
"docker:up": "docker compose --env-file .env --env-file .env.production up -d --no-build",
"docker:down": "docker compose --env-file .env --env-file .env.production down",
"docker:rmi": "docker compose --env-file .env --env-file .env.production stop nest-admin-server && docker container rm nest-admin-server && docker rmi nest-admin-server",
"docker:logs": "docker compose --env-file .env --env-file .env.production logs -f",
"c": "git add . && git cz && git push",
"release": "standard-version",
"commitlint": "commitlint --config commitlint.config.cjs -e -V",
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,md}\""
},
"dependencies": {
"@fastify/cookie": "^9.3.1",
"@fastify/multipart": "^8.1.0",
"@fastify/static": "^7.0.1",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@nestjs-modules/mailer": "^1.10.3",
"@nestjs/axios": "^3.0.2",
"@nestjs/bull": "^10.1.0",
"@nestjs/cache-manager": "^2.2.1",
"@nestjs/common": "^10.3.3",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.3.3",
"@nestjs/event-emitter": "^2.0.4",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-fastify": "^10.3.3",
"@nestjs/platform-socket.io": "^10.3.3",
"@nestjs/schedule": "^4.0.1",
"@nestjs/swagger": "^7.3.0",
"@nestjs/terminus": "^10.2.2",
"@nestjs/throttler": "^5.1.2",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.3.3",
"@socket.io/redis-adapter": "^8.2.1",
"@socket.io/redis-emitter": "^5.1.0",
"@types/lodash": "^4.14.202",
"axios": "^1.6.7",
"bull": "^4.12.2",
"cache-manager": "^5.4.0",
"cache-manager-ioredis-yet": "^1.2.2",
"chalk": "^5.3.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cron": "^3.1.6",
"cron-parser": "^4.9.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"dotenv": "16.4.4",
"handlebars": "^4.7.8",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"lodash": "^4.17.21",
"mysql2": "^3.9.1",
"nanoid": "^3.3.7",
"nodemailer": "^6.9.9",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"qiniu": "^7.11.0",
"reflect-metadata": "^0.2.1",
"rimraf": "^5.0.5",
"rxjs": "^7.8.1",
"socket.io": "^4.7.4",
"stacktrace-js": "^2.0.2",
"svg-captcha": "^1.4.0",
"systeminformation": "^5.22.0",
"typeorm": "0.3.17",
"ua-parser-js": "^1.0.37",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.6.4",
"@compodoc/compodoc": "^1.1.23",
"@nestjs/cli": "^10.3.2",
"@nestjs/schematics": "^10.1.1",
"@nestjs/testing": "^10.3.2",
"@types/cache-manager": "^4.0.6",
"@types/jest": "29.5.12",
"@types/multer": "^1.4.11",
"@types/node": "^20.11.16",
"@types/supertest": "^6.0.2",
"@types/ua-parser-js": "^0.7.39",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"jest": "^29.7.0",
"lint-staged": "^15.2.2",
"source-map-support": "^0.5.21",
"supertest": "^6.3.4",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3",
"cliui": "^8.0.1",
"commitizen": "^4.3.0",
"cz-customizable": "^7.0.0",
"standard-version": "^9.5.0",
"husky": "^8.0.0"
},
"pnpm": {
"overrides": {
"@liaoliaots/nestjs-redis": "npm:@songkeys/nestjs-redis"
}
},
"config": {
"commitizen": {
"path": "cz-customizable"
}
},
"lint-staged": {
"*": [
"npm run lint"
]
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"moduleNameMapper": {
"^~/(.*)$": "<rootDir>/$1"
},
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

12758
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

49
scripts/genEnvTypes.ts Normal file
View File

@ -0,0 +1,49 @@
import fs from 'node:fs'
import path from 'node:path'
import dotenv from 'dotenv'
const directoryPath = path.resolve(__dirname, '..')
const targets = ['.env', `.env.${process.env.NODE_ENV || 'development'}`]
const envObj = targets.reduce((prev, file) => {
const result = dotenv.parse(fs.readFileSync(path.join(directoryPath, file)))
return { ...prev, ...result }
}, {})
const envType = Object.entries<string>(envObj).reduce((prev, [key, value]) => {
return `${prev}
${key}: '${value}';`
}, '').trim()
fs.writeFile(path.join(directoryPath, 'types/env.d.ts'), `
// generate by ./scripts/generateEnvTypes.ts
declare global {
namespace NodeJS {
interface ProcessEnv {
${envType}
}
}
}
export {};
`, (err) => {
if (err)
console.log('生成 env.d.ts 文件失败')
else
console.log('成功生成 env.d.ts 文件')
})
// console.log('envObj:', envObj)
function formatValue(value) {
let _value
try {
const res = JSON.parse(value)
_value = typeof res === 'object' ? value : res
}
catch (error) {
_value = `'${value}'`
}
return _value
}

26
scripts/resetScheduler.ts Normal file
View File

@ -0,0 +1,26 @@
import { exec } from 'node:child_process'
import { CronJob } from 'cron'
/** 此文件仅供演示时使用 */
const runMigrationGenerate = async function () {
exec('npm run migration:revert && npm run migration:run', (error, stdout, stderr) => {
if (!error)
console.log('操作成功', error)
else
console.log('操作失败', error)
})
}
const job = CronJob.from({
/** 每天凌晨 4.30 恢复初始数据 */
cronTime: '30 4 * * *',
timeZone: 'Asia/Shanghai',
start: true,
onTick() {
runMigrationGenerate()
console.log('Task executed daily at 4.30 AM:', new Date().toLocaleTimeString())
},
})

67
src/app.module.ts Normal file
View File

@ -0,0 +1,67 @@
import { ClassSerializerInterceptor, Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
import config from '~/config'
import { SharedModule } from '~/shared/shared.module'
import { AllExceptionsFilter } from './common/filters/any-exception.filter'
import { IdempotenceInterceptor } from './common/interceptors/idempotence.interceptor'
import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor'
import { TransformInterceptor } from './common/interceptors/transform.interceptor'
import { AuthModule } from './modules/auth/auth.module'
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'
import { RbacGuard } from './modules/auth/guards/rbac.guard'
import { HealthModule } from './modules/health/health.module'
import { NetdiskModule } from './modules/netdisk/netdisk.module'
import { SseModule } from './modules/sse/sse.module'
import { SystemModule } from './modules/system/system.module'
import { TasksModule } from './modules/tasks/tasks.module'
import { TodoModule } from './modules/todo/todo.module'
import { ToolsModule } from './modules/tools/tools.module'
import { DatabaseModule } from './shared/database/database.module'
import { SocketModule } from './socket/socket.module'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
expandVariables: true,
// 指定多个 env 文件时,第一个优先级最高
envFilePath: ['.env.local', `.env.${process.env.NODE_ENV}`, '.env'],
load: [...Object.values(config)],
}),
SharedModule,
DatabaseModule,
AuthModule,
SystemModule,
TasksModule.forRoot(),
ToolsModule,
SocketModule,
HealthModule,
SseModule,
NetdiskModule,
// biz
// end biz
TodoModule,
],
providers: [
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
{ provide: APP_INTERCEPTOR, useClass: ClassSerializerInterceptor },
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
{ provide: APP_INTERCEPTOR, useFactory: () => new TimeoutInterceptor(15 * 1000) },
{ provide: APP_INTERCEPTOR, useClass: IdempotenceInterceptor },
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RbacGuard },
],
})
export class AppModule {}

View File

@ -0,0 +1,4 @@
<p>你的验证码是:</p>
<h2>{{code}}</h2>
<p>该验证码 10 分钟内有效,请勿将验证码告知给他人!</p>
<font color='grey'>本邮件由系统自动发出,请勿回复。</font>

View File

@ -0,0 +1,5 @@
<p>Your verification code is:</p>
<h1>{{verificationCode}}</h1>
<p>This code will expire in 10 minutes.</p>
<font color='grey'>This email is sent automatically by the system, please do not
reply.</font>

View File

@ -0,0 +1,46 @@
import FastifyCookie from '@fastify/cookie'
import FastifyMultipart from '@fastify/multipart'
import { FastifyAdapter } from '@nestjs/platform-fastify'
const app: FastifyAdapter = new FastifyAdapter({
trustProxy: true,
logger: false,
// forceCloseConnections: true,
})
export { app as fastifyApp }
app.register(FastifyMultipart, {
limits: {
fields: 10, // Max number of non-file fields
fileSize: 1024 * 1024 * 6, // limit size 6M
files: 5, // Max number of file fields
},
})
app.register(FastifyCookie, {
secret: 'cookie-secret', // 这个 secret 不太重要,不存鉴权相关,无关紧要
})
app.getInstance().addHook('onRequest', (request, reply, done) => {
// set undefined origin
const { origin } = request.headers
if (!origin)
request.headers.origin = request.headers.host
// forbidden php
const { url } = request
if (url.endsWith('.php')) {
reply.raw.statusMessage
= 'Eh. PHP is not support on this machine. Yep, I also think PHP is bestest programming language. But for me it is beyond my reach.'
return reply.code(418).send()
}
// skip favicon request
if (url.match(/favicon.ico$/) || url.match(/manifest.json$/))
return reply.code(204).send()
done()
})

View File

@ -0,0 +1,26 @@
import { INestApplication } from '@nestjs/common'
import { IoAdapter } from '@nestjs/platform-socket.io'
import { createAdapter } from '@socket.io/redis-adapter'
import { REDIS_PUBSUB } from '~/shared/redis/redis.constant'
export const RedisIoAdapterKey = 'm-shop-socket'
export class RedisIoAdapter extends IoAdapter {
constructor(private readonly app: INestApplication) {
super(app)
}
createIOServer(port: number, options?: any) {
const server = super.createIOServer(port, options)
const { pubClient, subClient } = this.app.get(REDIS_PUBSUB)
const redisAdapter = createAdapter(pubClient, subClient, {
key: RedisIoAdapterKey,
requestsTimeout: 10000,
})
server.adapter(redisAdapter)
return server
}
}

View File

@ -0,0 +1,83 @@
import { HttpStatus, Type, applyDecorators } from '@nestjs/common'
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger'
import { ResOp } from '~/common/model/response.model'
const baseTypeNames = ['String', 'Number', 'Boolean']
function genBaseProp(type: Type<any>) {
if (baseTypeNames.includes(type.name))
return { type: type.name.toLocaleLowerCase() }
else
return { $ref: getSchemaPath(type) }
}
/**
* @description:
*/
export function ApiResult<TModel extends Type<any>>({
type,
isPage,
status,
}: {
type?: TModel | TModel[]
isPage?: boolean
status?: HttpStatus
}) {
let prop = null
if (Array.isArray(type)) {
if (isPage) {
prop = {
type: 'object',
properties: {
items: {
type: 'array',
items: { $ref: getSchemaPath(type[0]) },
},
meta: {
type: 'object',
properties: {
itemCount: { type: 'number', default: 0 },
totalItems: { type: 'number', default: 0 },
itemsPerPage: { type: 'number', default: 0 },
totalPages: { type: 'number', default: 0 },
currentPage: { type: 'number', default: 0 },
},
},
},
}
}
else {
prop = {
type: 'array',
items: genBaseProp(type[0]),
}
}
}
else if (type) {
prop = genBaseProp(type)
}
else {
prop = { type: 'null', default: null }
}
const model = Array.isArray(type) ? type[0] : type
return applyDecorators(
ApiExtraModels(model),
ApiResponse({
status,
schema: {
allOf: [
{ $ref: getSchemaPath(ResOp) },
{
properties: {
data: prop,
},
},
],
},
}),
)
}

View File

@ -0,0 +1,10 @@
import { SetMetadata } from '@nestjs/common'
export const BYPASS_KEY = '__bypass_key__'
/**
*
*/
export function Bypass() {
return SetMetadata(BYPASS_KEY, true)
}

View File

@ -0,0 +1,8 @@
import type { ExecutionContext } from '@nestjs/common'
import { createParamDecorator } from '@nestjs/common'
import type { FastifyRequest } from 'fastify'
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<FastifyRequest>()
return data ? request.cookies?.[data] : request.cookies
})

View File

@ -0,0 +1,19 @@
import cluster from 'node:cluster'
import { Cron } from '@nestjs/schedule'
import { isMainProcess } from '~/global/env'
export const CronOnce: typeof Cron = (...rest): MethodDecorator => {
// If not in cluster mode, and PM2 main worker
if (isMainProcess)
// eslint-disable-next-line no-useless-call
return Cron.call(null, ...rest)
if (cluster.isWorker && cluster.worker?.id === 1)
// eslint-disable-next-line no-useless-call
return Cron.call(null, ...rest)
const returnNothing: MethodDecorator = () => {}
return returnNothing
}

View File

@ -0,0 +1,137 @@
import { applyDecorators } from '@nestjs/common'
import {
IsBoolean,
IsDate,
IsInt,
IsNotEmpty,
IsNumber,
IsOptional,
IsPositive,
IsString,
Max,
MaxLength,
Min,
MinLength,
} from 'class-validator'
import { isNumber } from 'lodash'
import {
ToArray,
ToBoolean,
ToDate,
ToLowerCase,
ToNumber,
ToTrim,
ToUpperCase,
} from './transform.decorator'
interface IOptionalOptions {
required?: boolean
}
interface INumberFieldOptions extends IOptionalOptions {
each?: boolean
int?: boolean
min?: number
max?: number
positive?: boolean
}
interface IStringFieldOptions extends IOptionalOptions {
each?: boolean
minLength?: number
maxLength?: number
lowerCase?: boolean
upperCase?: boolean
}
export function NumberField(
options: INumberFieldOptions = {},
): PropertyDecorator {
const { each, min, max, int, positive, required = true } = options
const decorators = [ToNumber()]
if (each)
decorators.push(ToArray())
if (int)
decorators.push(IsInt({ each }))
else
decorators.push(IsNumber({}, { each }))
if (isNumber(min))
decorators.push(Min(min, { each }))
if (isNumber(max))
decorators.push(Max(max, { each }))
if (positive)
decorators.push(IsPositive({ each }))
if (!required)
decorators.push(IsOptional())
return applyDecorators(...decorators)
}
export function StringField(
options: IStringFieldOptions = {},
): PropertyDecorator {
const {
each,
minLength,
maxLength,
lowerCase,
upperCase,
required = true,
} = options
const decorators = [IsString({ each }), ToTrim()]
if (each)
decorators.push(ToArray())
if (isNumber(minLength))
decorators.push(MinLength(minLength, { each }))
if (isNumber(maxLength))
decorators.push(MaxLength(maxLength, { each }))
if (lowerCase)
decorators.push(ToLowerCase())
if (upperCase)
decorators.push(ToUpperCase())
if (!required)
decorators.push(IsOptional())
else
decorators.push(IsNotEmpty({ each }))
return applyDecorators(...decorators)
}
export function BooleanField(
options: IOptionalOptions = {},
): PropertyDecorator {
const decorators = [ToBoolean(), IsBoolean()]
const { required = true } = options
if (!required)
decorators.push(IsOptional())
return applyDecorators(...decorators)
}
export function DateField(options: IOptionalOptions = {}): PropertyDecorator {
const decorators = [ToDate(), IsDate()]
const { required = true } = options
if (!required)
decorators.push(IsOptional())
return applyDecorators(...decorators)
}

View File

@ -0,0 +1,22 @@
import type { ExecutionContext } from '@nestjs/common'
import { createParamDecorator } from '@nestjs/common'
import type { FastifyRequest } from 'fastify'
import { getIp } from '~/utils/ip.util'
/**
* IP
*/
export const Ip = createParamDecorator((_, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest<FastifyRequest>()
return getIp(request)
})
/**
* request pathurl params
*/
export const Uri = createParamDecorator((_, context: ExecutionContext) => {
const request = context.switchToHttp().getRequest<FastifyRequest>()
return request.routerPath
})

View File

@ -0,0 +1,7 @@
import { HttpStatus, NotAcceptableException, Param, ParseIntPipe } from '@nestjs/common'
export function IdParam() {
return Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE, exceptionFactory: (_error) => {
throw new NotAcceptableException('id 格式不正确')
} }))
}

View File

@ -0,0 +1,15 @@
import { SetMetadata } from '@nestjs/common'
import { IdempotenceOption } from '../interceptors/idempotence.interceptor'
export const HTTP_IDEMPOTENCE_KEY = '__idempotence_key__'
export const HTTP_IDEMPOTENCE_OPTIONS = '__idempotence_options__'
/**
*
*/
export function Idempotence(options?: IdempotenceOption): MethodDecorator {
return function (target, key, descriptor: PropertyDescriptor) {
SetMetadata(HTTP_IDEMPOTENCE_OPTIONS, options || {})(descriptor.value)
}
}

View File

@ -0,0 +1,11 @@
import { applyDecorators } from '@nestjs/common'
import { ApiSecurity } from '@nestjs/swagger'
export const API_SECURITY_AUTH = 'auth'
/**
* like to @ApiSecurity('auth')
*/
export function ApiSecurityAuth(): ClassDecorator & MethodDecorator {
return applyDecorators(ApiSecurity(API_SECURITY_AUTH))
}

View File

@ -0,0 +1,146 @@
import { Transform } from 'class-transformer'
import { castArray, isArray, isNil, trim } from 'lodash'
/**
* convert string to number
*/
export function ToNumber(): PropertyDecorator {
return Transform(
(params) => {
const value = params.value as string[] | string
if (isArray(value))
return value.map(v => Number(v))
return Number(value)
},
{ toClassOnly: true },
)
}
/**
* convert string to int
*/
export function ToInt(): PropertyDecorator {
return Transform(
(params) => {
const value = params.value as string[] | string
if (isArray(value))
return value.map(v => Number.parseInt(v))
return Number.parseInt(value)
},
{ toClassOnly: true },
)
}
/**
* convert string to boolean
*/
export function ToBoolean(): PropertyDecorator {
return Transform(
(params) => {
switch (params.value) {
case 'true':
return true
case 'false':
return false
default:
return params.value
}
},
{ toClassOnly: true },
)
}
/**
* convert string to Date
*/
export function ToDate(): PropertyDecorator {
return Transform(
(params) => {
const { value } = params
if (!value)
return
return new Date(value)
},
{ toClassOnly: true },
)
}
/**
* transforms to array, specially for query params
*/
export function ToArray(): PropertyDecorator {
return Transform(
(params) => {
const { value } = params
if (isNil(value))
return []
return castArray(value)
},
{ toClassOnly: true },
)
}
/**
* trim spaces from start and end, replace multiple spaces with one.
*/
export function ToTrim(): PropertyDecorator {
return Transform(
(params) => {
const value = params.value as string[] | string
if (isArray(value))
return value.map(v => trim(v))
return trim(value)
},
{ toClassOnly: true },
)
}
/**
* lowercase value
*/
export function ToLowerCase(): PropertyDecorator {
return Transform(
(params) => {
const value = params.value as string[] | string
if (!value)
return
if (isArray(value))
return value.map(v => v.toLowerCase())
return value.toLowerCase()
},
{ toClassOnly: true },
)
}
/**
* uppercase value
*/
export function ToUpperCase(): PropertyDecorator {
return Transform(
(params) => {
const value = params.value as string[] | string
if (!value)
return
if (isArray(value))
return value.map(v => v.toUpperCase())
return value.toUpperCase()
},
{ toClassOnly: true },
)
}

View File

@ -0,0 +1,26 @@
import { ApiProperty } from '@nestjs/swagger'
import { Expose, Transform } from 'class-transformer'
import { IsInt, IsOptional, Max, Min } from 'class-validator'
export class CursorDto<T = any> {
@ApiProperty({ minimum: 0, default: 0 })
@Min(0)
@IsInt()
@Expose()
@IsOptional({ always: true })
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 0), {
toClassOnly: true,
})
cursor?: number
@ApiProperty({ minimum: 1, maximum: 100, default: 10 })
@Min(1)
@Max(100)
@IsInt()
@IsOptional({ always: true })
@Expose()
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), {
toClassOnly: true,
})
limit?: number
}

View File

@ -0,0 +1,8 @@
import { IsDefined, IsNotEmpty, IsNumber } from 'class-validator'
export class BatchDeleteDto {
@IsDefined()
@IsNotEmpty()
@IsNumber({}, { each: true })
ids: number[]
}

6
src/common/dto/id.dto.ts Normal file
View File

@ -0,0 +1,6 @@
import { IsNumber } from 'class-validator'
export class IdDto {
@IsNumber()
id: number
}

View File

@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger'
import { Expose, Transform } from 'class-transformer'
import { Allow, IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'
export enum Order {
ASC = 'ASC',
DESC = 'DESC',
}
export class PagerDto<T = any> {
@ApiProperty({ minimum: 1, default: 1 })
@Min(1)
@IsInt()
@Expose()
@IsOptional({ always: true })
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 1), {
toClassOnly: true,
})
page?: number
@ApiProperty({ minimum: 1, maximum: 100, default: 10 })
@Min(1)
@Max(100)
@IsInt()
@IsOptional({ always: true })
@Expose()
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), {
toClassOnly: true,
})
pageSize?: number
@ApiProperty()
@IsString()
@IsOptional()
field?: string // | keyof T
@ApiProperty({ enum: Order })
@IsEnum(Order)
@IsOptional()
@Transform(({ value }) => (value === 'asc' ? Order.ASC : Order.DESC))
order?: Order
@Allow()
_t?: number
}

View File

@ -0,0 +1,55 @@
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'
import { Exclude } from 'class-transformer'
import {
BaseEntity,
Column,
CreateDateColumn,
PrimaryGeneratedColumn,
UpdateDateColumn,
VirtualColumn,
} from 'typeorm'
// 如果觉得前端转换时间太麻烦并且不考虑通用性的话可以在服务端进行转换eg: @UpdateDateColumn({ name: 'updated_at', transformer })
// const transformer: ValueTransformer = {
// to(value) {
// return value
// },
// from(value) {
// return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
// },
// }
export abstract class CommonEntity extends BaseEntity {
@PrimaryGeneratedColumn()
id: number
@CreateDateColumn({ name: 'created_at' })
createdAt: Date
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date
}
export abstract class CompleteEntity extends CommonEntity {
@ApiHideProperty()
@Exclude()
@Column({ name: 'create_by', update: false, comment: '创建者' })
createBy: number
@ApiHideProperty()
@Exclude()
@Column({ name: 'update_by', comment: '更新者' })
updateBy: number
/**
* service
* @see https://typeorm.io/decorator-reference#virtualcolumn
*/
@ApiProperty({ description: '创建者' })
@VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.create_by` })
creator: string
@ApiProperty({ description: '更新者' })
@VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.update_by` })
updater: string
}

View File

@ -0,0 +1,40 @@
import { HttpException, HttpStatus } from '@nestjs/common'
import { ErrorEnum } from '~/constants/error-code.constant'
import { RESPONSE_SUCCESS_CODE } from '~/constants/response.constant'
export class BusinessException extends HttpException {
private errorCode: number
constructor(error: ErrorEnum | string) {
// 如果是非 ErrorEnum
if (!error.includes(':')) {
super(
HttpException.createBody({
code: RESPONSE_SUCCESS_CODE,
message: error,
}),
HttpStatus.OK,
)
this.errorCode = RESPONSE_SUCCESS_CODE
return
}
const [code, message] = error.split(':')
super(
HttpException.createBody({
code,
message,
}),
HttpStatus.OK,
)
this.errorCode = Number(code)
}
getErrorCode(): number {
return this.errorCode
}
}
export { BusinessException as BizException }

View File

@ -0,0 +1,10 @@
import { NotFoundException } from '@nestjs/common'
import { sample } from 'lodash'
export const NotFoundMessage = ['404, Not Found']
export class CannotFindException extends NotFoundException {
constructor() {
super(sample(NotFoundMessage))
}
}

View File

@ -0,0 +1,38 @@
import { HttpException } from '@nestjs/common'
import { WsException } from '@nestjs/websockets'
import { ErrorEnum } from '~/constants/error-code.constant'
export class SocketException extends WsException {
private errorCode: number
constructor(message: string)
constructor(error: ErrorEnum)
constructor(...args: any) {
const error = args[0]
if (typeof error === 'string') {
super(
HttpException.createBody({
code: 0,
message: error,
}),
)
this.errorCode = 0
return
}
const [code, message] = error.split(':')
super(
HttpException.createBody({
code,
message,
}),
)
this.errorCode = Number(code)
}
getErrorCode(): number {
return this.errorCode
}
}

View File

@ -0,0 +1,89 @@
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common'
import { FastifyReply, FastifyRequest } from 'fastify'
import { BusinessException } from '~/common/exceptions/biz.exception'
import { ErrorEnum } from '~/constants/error-code.constant'
import { isDev } from '~/global/env'
interface myError {
readonly status: number
readonly statusCode?: number
readonly message?: string
}
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private readonly logger = new Logger(AllExceptionsFilter.name)
constructor() {
this.registerCatchAllExceptionsHook()
}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const request = ctx.getRequest<FastifyRequest>()
const response = ctx.getResponse<FastifyReply>()
const url = request.raw.url!
const status
= exception instanceof HttpException
? exception.getStatus()
: (exception as myError)?.status
|| (exception as myError)?.statusCode
|| HttpStatus.INTERNAL_SERVER_ERROR
let message
= (exception as any)?.response?.message
|| (exception as myError)?.message
|| `${exception}`
// 系统内部错误时
if (
status === HttpStatus.INTERNAL_SERVER_ERROR
&& !(exception instanceof BusinessException)
) {
Logger.error(exception, undefined, 'Catch')
// 生产环境下隐藏错误信息
if (!isDev)
message = ErrorEnum.SERVER_ERROR?.split(':')[1]
}
else {
this.logger.warn(
`错误信息:(${status}) ${message} Path: ${decodeURI(url)}`,
)
}
const apiErrorCode: number
= exception instanceof BusinessException ? exception.getErrorCode() : status
// 返回基础响应结果
const resBody: IBaseResponse = {
code: apiErrorCode,
message,
data: null,
}
response.status(status).send(resBody)
}
registerCatchAllExceptionsHook() {
process.on('unhandledRejection', (reason) => {
console.error('unhandledRejection: ', reason)
})
process.on('uncaughtException', (err) => {
console.error('uncaughtException: ', err)
})
}
}

View File

@ -0,0 +1,148 @@
import type {
CallHandler,
ExecutionContext,
NestInterceptor,
} from '@nestjs/common'
import {
ConflictException,
Injectable,
SetMetadata,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import type { FastifyRequest } from 'fastify'
import { catchError, tap } from 'rxjs'
import { CacheService } from '~/shared/redis/cache.service'
import { hashString } from '~/utils'
import { getIp } from '~/utils/ip.util'
import { getRedisKey } from '~/utils/redis.util'
import { HTTP_IDEMPOTENCE_KEY, HTTP_IDEMPOTENCE_OPTIONS } from '../decorators/idempotence.decorator'
const IdempotenceHeaderKey = 'x-idempotence'
export interface IdempotenceOption {
errorMessage?: string
pendingMessage?: string
/**
*
*/
handler?: (req: FastifyRequest) => any
/**
*
* @default 60
*/
expired?: number
/**
* header key request key key
*/
generateKey?: (req: FastifyRequest) => string
/**
* header key
* @default false
*/
disableGenerateKey?: boolean
}
@Injectable()
export class IdempotenceInterceptor implements NestInterceptor {
constructor(
private readonly reflector: Reflector,
private readonly cacheService: CacheService,
) {}
async intercept(context: ExecutionContext, next: CallHandler) {
const request = context.switchToHttp().getRequest<FastifyRequest>()
// skip Get 请求
if (request.method.toUpperCase() === 'GET')
return next.handle()
const handler = context.getHandler()
const options: IdempotenceOption | undefined = this.reflector.get(
HTTP_IDEMPOTENCE_OPTIONS,
handler,
)
if (!options)
return next.handle()
const {
errorMessage = '相同请求成功后在 60 秒内只能发送一次',
pendingMessage = '相同请求正在处理中...',
handler: errorHandler,
expired = 60,
disableGenerateKey = false,
} = options
const redis = this.cacheService.getClient()
const idempotence = request.headers[IdempotenceHeaderKey] as string
const key = disableGenerateKey
? undefined
: options.generateKey
? options.generateKey(request)
: this.generateKey(request)
const idempotenceKey
= !!(idempotence || key) && getRedisKey(`idempotence:${idempotence || key}`)
SetMetadata(HTTP_IDEMPOTENCE_KEY, idempotenceKey)(handler)
if (idempotenceKey) {
const resultValue: '0' | '1' | null = (await redis.get(
idempotenceKey,
)) as any
if (resultValue !== null) {
if (errorHandler)
return await errorHandler(request)
const message = {
1: errorMessage,
0: pendingMessage,
}[resultValue]
throw new ConflictException(message)
}
else {
await redis.set(idempotenceKey, '0', 'EX', expired)
}
}
return next.handle().pipe(
tap(async () => {
idempotenceKey && (await redis.set(idempotenceKey, '1', 'KEEPTTL'))
}),
catchError(async (err) => {
if (idempotenceKey)
await redis.del(idempotenceKey)
throw err
}),
)
}
private generateKey(req: FastifyRequest) {
const { body, params, query = {}, headers, url } = req
const obj = { body, url, params, query } as any
const uuid = headers['x-uuid']
if (uuid) {
obj.uuid = uuid
}
else {
const ua = headers['user-agent']
const ip = getIp(req)
if (!ua && !ip)
return undefined
Object.assign(obj, { ua, ip })
}
return hashString(JSON.stringify(obj))
}
}

View File

@ -0,0 +1,35 @@
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common'
import { Observable, tap } from 'rxjs'
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private logger = new Logger(LoggingInterceptor.name, { timestamp: false })
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
const call$ = next.handle()
const request = context.switchToHttp().getRequest()
const content = `${request.method} -> ${request.url}`
const isSse = request.headers.accept === 'text/event-stream'
this.logger.debug(`+++ 请求:${content}`)
const now = Date.now()
return call$.pipe(
tap(() => {
if (isSse)
return
this.logger.debug(`--- 响应:${content}${` +${Date.now() - now}ms`}`)
},
),
)
}
}

View File

@ -0,0 +1,26 @@
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
RequestTimeoutException,
} from '@nestjs/common'
import { Observable, TimeoutError, throwError } from 'rxjs'
import { catchError, timeout } from 'rxjs/operators'
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(private readonly time: number = 10000) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(this.time),
catchError((err) => {
if (err instanceof TimeoutError)
return throwError(new RequestTimeoutException('请求超时'))
return throwError(err)
}),
)
}
}

View File

@ -0,0 +1,46 @@
import {
CallHandler,
ExecutionContext,
HttpStatus,
Injectable,
NestInterceptor,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
import { ResOp } from '~/common/model/response.model'
import { BYPASS_KEY } from '../decorators/bypass.decorator'
/**
* @Bypass
*/
@Injectable()
export class TransformInterceptor implements NestInterceptor {
constructor(private readonly reflector: Reflector) {}
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> {
const bypass = this.reflector.get<boolean>(
BYPASS_KEY,
context.getHandler(),
)
if (bypass)
return next.handle()
return next.handle().pipe(
map((data) => {
// if (typeof data === 'undefined') {
// context.switchToHttp().getResponse().status(HttpStatus.NO_CONTENT);
// return data;
// }
return new ResOp(HttpStatus.OK, data ?? null)
}),
)
}
}

View File

@ -0,0 +1,42 @@
import { ApiProperty } from '@nestjs/swagger'
import {
RESPONSE_SUCCESS_CODE,
RESPONSE_SUCCESS_MSG,
} from '~/constants/response.constant'
export class ResOp<T = any> {
@ApiProperty({ type: 'object' })
data?: T
@ApiProperty({ type: 'number', default: RESPONSE_SUCCESS_CODE })
code: number
@ApiProperty({ type: 'string', default: RESPONSE_SUCCESS_MSG })
message: string
constructor(code: number, data: T, message = RESPONSE_SUCCESS_MSG) {
this.code = code
this.data = data
this.message = message
}
static success<T>(data?: T, message?: string) {
return new ResOp(RESPONSE_SUCCESS_CODE, data, message)
}
static error(code: number, message) {
return new ResOp(code, {}, message)
}
}
export class TreeResult<T> {
@ApiProperty()
id: number
@ApiProperty()
parentId: number
@ApiProperty()
children?: TreeResult<T>[]
}

View File

@ -0,0 +1,18 @@
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common'
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = Number.parseInt(value, 10)
if (Number.isNaN(val))
throw new BadRequestException('id validation failed')
return val
}
}

20
src/config/app.config.ts Normal file
View File

@ -0,0 +1,20 @@
import { ConfigType, registerAs } from '@nestjs/config'
import { env, envNumber } from '~/global/env'
export const appRegToken = 'app'
export const AppConfig = registerAs(appRegToken, () => ({
name: env('APP_NAME'),
port: envNumber('APP_PORT', 3000),
baseUrl: env('APP_BASE_URL'),
globalPrefix: env('GLOBAL_PREFIX', 'api'),
locale: env('APP_LOCALE', 'zh-CN'),
logger: {
level: env('LOGGER_LEVEL'),
maxFiles: envNumber('LOGGER_MAX_FILES'),
},
}))
export type IAppConfig = ConfigType<typeof AppConfig>

View File

@ -0,0 +1,40 @@
import { ConfigType, registerAs } from '@nestjs/config'
import { DataSource, DataSourceOptions } from 'typeorm'
import { env, envBoolean, envNumber } from '~/global/env'
// eslint-disable-next-line import/order
import dotenv from 'dotenv'
dotenv.config({ path: `.env.${process.env.NODE_ENV}` })
// 当前通过 npm scripts 执行的命令
const currentScript = process.env.npm_lifecycle_event
const dataSourceOptions: DataSourceOptions = {
type: 'mysql',
host: env('DB_HOST', '127.0.0.1'),
port: envNumber('DB_PORT', 3306),
username: env('DB_USERNAME'),
password: env('DB_PASSWORD'),
database: env('DB_DATABASE'),
synchronize: envBoolean('DB_SYNCHRONIZE', false),
// 解决通过 pnpm migration:run 初始化数据时,遇到的 SET FOREIGN_KEY_CHECKS = 0; 等语句报错问题, 仅在执行数据迁移操作时设为 true
multipleStatements: currentScript === 'typeorm',
entities: ['dist/modules/**/*.entity{.ts,.js}'],
migrations: ['dist/migrations/*{.ts,.js}'],
subscribers: ['dist/modules/**/*.subscriber{.ts,.js}'],
}
export const dbRegToken = 'database'
export const DatabaseConfig = registerAs(
dbRegToken,
(): DataSourceOptions => dataSourceOptions,
)
export type IDatabaseConfig = ConfigType<typeof DatabaseConfig>
const dataSource = new DataSource(dataSourceOptions)
export default dataSource

37
src/config/index.ts Normal file
View File

@ -0,0 +1,37 @@
import { AppConfig, IAppConfig, appRegToken } from './app.config'
import { DatabaseConfig, IDatabaseConfig, dbRegToken } from './database.config'
import { IMailerConfig, MailerConfig, mailerRegToken } from './mailer.config'
import { IOssConfig, OssConfig, ossRegToken } from './oss.config'
import { IRedisConfig, RedisConfig, redisRegToken } from './redis.config'
import { ISecurityConfig, SecurityConfig, securityRegToken } from './security.config'
import { ISwaggerConfig, SwaggerConfig, swaggerRegToken } from './swagger.config'
export * from './app.config'
export * from './redis.config'
export * from './database.config'
export * from './swagger.config'
export * from './security.config'
export * from './mailer.config'
export * from './oss.config'
export interface AllConfigType {
[appRegToken]: IAppConfig
[dbRegToken]: IDatabaseConfig
[mailerRegToken]: IMailerConfig
[redisRegToken]: IRedisConfig
[securityRegToken]: ISecurityConfig
[swaggerRegToken]: ISwaggerConfig
[ossRegToken]: IOssConfig
}
export type ConfigKeyPaths = RecordNamePaths<AllConfigType>
export default {
AppConfig,
DatabaseConfig,
MailerConfig,
OssConfig,
RedisConfig,
SecurityConfig,
SwaggerConfig,
}

View File

@ -0,0 +1,18 @@
import { ConfigType, registerAs } from '@nestjs/config'
import { env, envNumber } from '~/global/env'
export const mailerRegToken = 'mailer'
export const MailerConfig = registerAs(mailerRegToken, () => ({
host: env('SMTP_HOST'),
port: envNumber('SMTP_PORT'),
ignoreTLS: true,
secure: true,
auth: {
user: env('SMTP_USER'),
pass: env('SMTP_PASS'),
},
}))
export type IMailerConfig = ConfigType<typeof MailerConfig>

32
src/config/oss.config.ts Normal file
View File

@ -0,0 +1,32 @@
import { ConfigType, registerAs } from '@nestjs/config'
import * as qiniu from 'qiniu'
import { env } from '~/global/env'
function 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 const ossRegToken = 'oss'
export const OssConfig = registerAs(ossRegToken, () => ({
accessKey: env('OSS_ACCESSKEY'),
secretKey: env('OSS_SECRETKEY'),
domain: env('OSS_DOMAIN'),
bucket: env('OSS_BUCKET'),
zone: parseZone(env('OSS_ZONE') || 'Zone_z2'),
access: (env('OSS_ACCESS_TYPE') as any) || 'public',
}))
export type IOssConfig = ConfigType<typeof OssConfig>

View File

@ -0,0 +1,14 @@
import { ConfigType, registerAs } from '@nestjs/config'
import { env, envNumber } from '~/global/env'
export const redisRegToken = 'redis'
export const RedisConfig = registerAs(redisRegToken, () => ({
host: env('REDIS_HOST', '127.0.0.1'),
port: envNumber('REDIS_PORT', 6379),
password: env('REDIS_PASSWORD'),
db: envNumber('REDIS_DB'),
}))
export type IRedisConfig = ConfigType<typeof RedisConfig>

View File

@ -0,0 +1,14 @@
import { ConfigType, registerAs } from '@nestjs/config'
import { env, envNumber } from '~/global/env'
export const securityRegToken = 'security'
export const SecurityConfig = registerAs(securityRegToken, () => ({
jwtSecret: env('JWT_SECRET'),
jwtExprire: envNumber('JWT_EXPIRE'),
refreshSecret: env('REFRESH_TOKEN_SECRET'),
refreshExpire: envNumber('REFRESH_TOKEN_EXPIRE'),
}))
export type ISecurityConfig = ConfigType<typeof SecurityConfig>

View File

@ -0,0 +1,12 @@
import { ConfigType, registerAs } from '@nestjs/config'
import { env, envBoolean } from '~/global/env'
export const swaggerRegToken = 'swagger'
export const SwaggerConfig = registerAs(swaggerRegToken, () => ({
enable: envBoolean('SWAGGER_ENABLE'),
path: env('SWAGGER_PATH'),
}))
export type ISwaggerConfig = ConfigType<typeof SwaggerConfig>

View File

@ -0,0 +1,8 @@
export enum RedisKeys {
AccessIp = 'access_ip',
CAPTCHA_IMG_PREFIX = 'captcha:img:',
AUTH_TOKEN_PREFIX = 'auth:token:',
AUTH_PERM_PREFIX = 'auth:permission:',
AUTH_PASSWORD_V_PREFIX = 'auth:passwordVersion:',
}
export const API_CACHE_PREFIX = 'api-cache:'

View File

@ -0,0 +1,49 @@
export enum ErrorEnum {
DEFAULT = '0:未知错误',
SERVER_ERROR = '500:服务繁忙,请稍后再试',
SYSTEM_USER_EXISTS = '1001:系统用户已存在',
INVALID_VERIFICATION_CODE = '1002:验证码填写有误',
INVALID_USERNAME_PASSWORD = '1003:用户名密码有误',
NODE_ROUTE_EXISTS = '1004:节点路由已存在',
PERMISSION_REQUIRES_PARENT = '1005:权限必须包含父节点',
ILLEGAL_OPERATION_DIRECTORY_PARENT = '1006:非法操作:该节点仅支持目录类型父节点',
ILLEGAL_OPERATION_CANNOT_CONVERT_NODE_TYPE = '1007:非法操作:节点类型无法直接转换',
ROLE_HAS_ASSOCIATED_USERS = '1008:该角色存在关联用户,请先删除关联用户',
DEPARTMENT_HAS_ASSOCIATED_USERS = '1009:该部门存在关联用户,请先删除关联用户',
DEPARTMENT_HAS_ASSOCIATED_ROLES = '1010:该部门存在关联角色,请先删除关联角色',
PASSWORD_MISMATCH = '1011:旧密码与原密码不一致',
LOGOUT_OWN_SESSION = '1012:如想下线自身可右上角退出',
NOT_ALLOWED_TO_LOGOUT_USER = '1013:不允许下线该用户',
PARENT_MENU_NOT_FOUND = '1014:父级菜单不存在',
DEPARTMENT_HAS_CHILD_DEPARTMENTS = '1015:该部门存在子部门,请先删除子部门',
SYSTEM_BUILTIN_FUNCTION_NOT_ALLOWED = '1016:系统内置功能不允许操作',
USER_NOT_FOUND = '1017:用户不存在',
UNABLE_TO_FIND_DEPARTMENT_FOR_USER = '1018:无法查找当前用户所属部门',
DEPARTMENT_NOT_FOUND = '1019:部门不存在',
DICT_NAME_EXISTS = '1020: 已存在相同名称的字典',
PARAMETER_CONFIG_KEY_EXISTS = '1021:参数配置键值对已存在',
DEFAULT_ROLE_NOT_FOUND = '1022:所分配的默认角色不存在',
INVALID_LOGIN = '1101:登录无效,请重新登录',
NO_PERMISSION = '1102:无权限访问',
ONLY_ADMIN_CAN_LOGIN = '1103:不是管理员,无法登录',
REQUEST_INVALIDATED = '1104:当前请求已失效',
ACCOUNT_LOGGED_IN_ELSEWHERE = '1105:您的账号已在其他地方登录',
GUEST_ACCOUNT_RESTRICTED_OPERATION = '1106:游客账号不允许操作',
REQUESTED_RESOURCE_NOT_FOUND = '1107:所请求的资源不存在',
TOO_MANY_REQUESTS = '1201:请求频率过快,请一分钟后再试',
MAXIMUM_FIVE_VERIFICATION_CODES_PER_DAY = '1202:一天最多发送5条验证码',
VERIFICATION_CODE_SEND_FAILED = '1203:验证码发送失败',
INSECURE_MISSION = '1301:不安全的任务,确保执行的加入@Mission注解',
EXECUTED_MISSION_NOT_FOUND = '1302:所执行的任务不存在',
MISSION_EXECUTION_FAILED = '1303:任务执行失败',
MISSION_NOT_FOUND = '1304:任务不存在',
// OSS相关
OSS_FILE_OR_DIR_EXIST = '1401:当前创建的文件或目录已存在',
OSS_NO_OPERATION_REQUIRED = '1402:无需操作',
OSS_EXCEE_MAXIMUM_QUANTITY = '1403:已超出支持的最大处理数量',
}

View File

@ -0,0 +1,4 @@
export enum EventBusEvents {
TokenExpired = 'token.expired',
SystemException = 'system.exception',
}

View File

@ -0,0 +1,8 @@
export const OSS_CONFIG = 'admin_module:qiniu_config'
export const OSS_API = 'http://api.qiniu.com'
// 目录分隔符
export const NETDISK_DELIMITER = '/'
export const NETDISK_LIMIT = 100
export const NETDISK_HANDLE_MAX_ITEM = 1000
export const NETDISK_COPY_SUFFIX = '的副本'

View File

@ -0,0 +1,15 @@
export const RESPONSE_SUCCESS_CODE = 200
export const RESPONSE_SUCCESS_MSG = 'success'
/**
* @description: contentType
*/
export enum ContentTypeEnum {
// json
JSON = 'application/json;charset=UTF-8',
// form-data qs
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data upload
FORM_DATA = 'multipart/form-data;charset=UTF-8',
}

View File

@ -0,0 +1,6 @@
export const SYS_USER_INITPASSWORD = 'sys_user_initPassword'
export const SYS_API_TOKEN = 'sys_api_token'
/** 超级管理员用户 id */
export const ROOT_USER_ID = 1
/** 超级管理员角色 id */
export const ROOT_ROLE_ID = 1

62
src/global/env.ts Normal file
View File

@ -0,0 +1,62 @@
import cluster from 'node:cluster'
export const isMainCluster
= process.env.NODE_APP_INSTANCE && Number.parseInt(process.env.NODE_APP_INSTANCE) === 0
export const isMainProcess = cluster.isPrimary || isMainCluster
export const isDev = process.env.NODE_ENV === 'development'
export const isTest = !!process.env.TEST
export const cwd = process.cwd()
/**
*
*/
export type BaseType = boolean | number | string | undefined | null
/**
*
* @param key
* @param defaultValue
* @param callback
*/
function fromatValue<T extends BaseType = string>(key: string, defaultValue: T, callback?: (value: string) => T): T {
const value: string | undefined = process.env[key]
if (typeof value === 'undefined')
return defaultValue
if (!callback)
return value as unknown as T
return callback(value)
}
export function env(key: string, defaultValue: string = '') {
return fromatValue(key, defaultValue)
}
export function envString(key: string, defaultValue: string = '') {
return fromatValue(key, defaultValue)
}
export function envNumber(key: string, defaultValue: number = 0) {
return fromatValue(key, defaultValue, (value) => {
try {
return Number(value)
}
catch {
throw new Error(`${key} environment variable is not a number`)
}
})
}
export function envBoolean(key: string, defaultValue: boolean = false) {
return fromatValue(key, defaultValue, (value) => {
try {
return Boolean(JSON.parse(value))
}
catch {
throw new Error(`${key} environment variable is not a boolean`)
}
})
}

5
src/helper/catchError.ts Normal file
View File

@ -0,0 +1,5 @@
export function catchError() {
process.on('unhandledRejection', (reason, p) => {
console.log('Promise: ', p, 'Reason: ', reason)
})
}

View File

@ -0,0 +1,40 @@
import { NotFoundException } from '@nestjs/common'
import { ObjectLiteral, Repository } from 'typeorm'
import { PagerDto } from '~/common/dto/pager.dto'
import { paginate } from '../paginate'
import { Pagination } from '../paginate/pagination'
export class BaseService<E extends ObjectLiteral, R extends Repository<E> = Repository<E>> {
constructor(private repository: R) {
}
async list({
page,
pageSize,
}: PagerDto): Promise<Pagination<E>> {
return paginate(this.repository, { page, pageSize })
}
async findOne(id: number): Promise<E> {
const item = await this.repository.createQueryBuilder().where({ id }).getOne()
if (!item)
throw new NotFoundException('未找到该记录')
return item
}
async create(dto: any): Promise<E> {
return await this.repository.save(dto)
}
async update(id: number, dto: any): Promise<void> {
await this.repository.update(id, dto)
}
async delete(id: number): Promise<void> {
const item = await this.findOne(id)
await this.repository.remove(item)
}
}

View File

@ -0,0 +1,81 @@
import type { Type } from '@nestjs/common'
import {
Body,
Controller,
Delete,
Get,
Patch,
Post,
Put,
Query,
} from '@nestjs/common'
import { ApiBody, IntersectionType, PartialType } from '@nestjs/swagger'
import pluralize from 'pluralize'
import { ApiResult } from '~/common/decorators/api-result.decorator'
import { IdParam } from '~/common/decorators/id-param.decorator'
import { PagerDto } from '~/common/dto/pager.dto'
import { BaseService } from './base.service'
export function BaseCrudFactory<
E extends new (...args: any[]) => any,
>({ entity, dto, permissions }: { entity: E, dto?: Type<any>, permissions?: Record<string, string> }): Type<any> {
const prefix = entity.name.toLowerCase().replace(/entity$/, '')
const pluralizeName = pluralize(prefix) as string
dto = dto ?? class extends entity {}
class Dto extends dto {}
class UpdateDto extends PartialType(Dto) {}
class QueryDto extends IntersectionType(PagerDto, PartialType(Dto)) {}
permissions = permissions ?? {
LIST: `${prefix}:list`,
CREATE: `${prefix}:create`,
READ: `${prefix}:read`,
UPDATE: `${prefix}:update`,
DELETE: `${prefix}:delete`,
} as const
@Controller(pluralizeName)
class BaseController<S extends BaseService<E>> {
constructor(private service: S) { }
@Get()
@ApiResult({ type: [entity], isPage: true })
async list(@Query() pager: QueryDto) {
return await this.service.list(pager)
}
@Get(':id')
@ApiResult({ type: entity })
async get(@IdParam() id: number) {
return await this.service.findOne(id)
}
@Post()
@ApiBody({ type: dto })
async create(@Body() dto: Dto) {
return await this.service.create(dto)
}
@Put(':id')
async update(@IdParam() id: number, @Body() dto: UpdateDto) {
return await this.service.update(id, dto)
}
@Patch(':id')
async patch(@IdParam() id: number, @Body() dto: UpdateDto) {
await this.service.update(id, dto)
}
@Delete(':id')
async delete(@IdParam() id: number) {
await this.service.delete(id)
}
}
return BaseController
}

19
src/helper/genRedisKey.ts Normal file
View File

@ -0,0 +1,19 @@
import { RedisKeys } from '~/constants/cache.constant'
/** 生成验证码 redis key */
export function genCaptchaImgKey(val: string | number) {
return `${RedisKeys.CAPTCHA_IMG_PREFIX}${String(val)}` as const
}
/** 生成 auth token redis key */
export function genAuthTokenKey(val: string | number) {
return `${RedisKeys.AUTH_TOKEN_PREFIX}${String(val)}` as const
}
/** 生成 auth permission redis key */
export function genAuthPermKey(val: string | number) {
return `${RedisKeys.AUTH_PERM_PREFIX}${String(val)}` as const
}
/** 生成 auth passwordVersion redis key */
export function genAuthPVKey(val: string | number) {
return `${RedisKeys.AUTH_PASSWORD_V_PREFIX}${String(val)}` as const
}

View File

@ -0,0 +1,27 @@
import { IPaginationMeta } from './interface'
import { Pagination } from './pagination'
export function createPaginationObject<T>({
items,
totalItems,
currentPage,
limit,
}: {
items: T[]
totalItems?: number
currentPage: number
limit: number
}): Pagination<T> {
const totalPages
= totalItems !== undefined ? Math.ceil(totalItems / limit) : undefined
const meta: IPaginationMeta = {
totalItems,
itemCount: items.length,
itemsPerPage: limit,
totalPages,
currentPage,
}
return new Pagination<T>(items, meta)
}

View File

@ -0,0 +1,147 @@
import {
FindManyOptions,
FindOptionsWhere,
ObjectLiteral,
Repository,
SelectQueryBuilder,
} from 'typeorm'
import { createPaginationObject } from './create-pagination'
import { IPaginationOptions, PaginationTypeEnum } from './interface'
import { Pagination } from './pagination'
const DEFAULT_LIMIT = 10
const DEFAULT_PAGE = 1
function resolveOptions(
options: IPaginationOptions,
): [number, number, PaginationTypeEnum] {
const { page, pageSize, paginationType } = options
return [
page || DEFAULT_PAGE,
pageSize || DEFAULT_LIMIT,
paginationType || PaginationTypeEnum.TAKE_AND_SKIP,
]
}
async function paginateRepository<T>(
repository: Repository<T>,
options: IPaginationOptions,
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>,
): Promise<Pagination<T>> {
const [page, limit] = resolveOptions(options)
const promises: [Promise<T[]>, Promise<number> | undefined] = [
repository.find({
skip: limit * (page - 1),
take: limit,
...searchOptions,
}),
undefined,
]
const [items, total] = await Promise.all(promises)
return createPaginationObject<T>({
items,
totalItems: total,
currentPage: page,
limit,
})
}
async function paginateQueryBuilder<T>(
queryBuilder: SelectQueryBuilder<T>,
options: IPaginationOptions,
): Promise<Pagination<T>> {
const [page, limit, paginationType] = resolveOptions(options)
if (paginationType === PaginationTypeEnum.TAKE_AND_SKIP)
queryBuilder.take(limit).skip((page - 1) * limit)
else
queryBuilder.limit(limit).offset((page - 1) * limit)
const [items, total] = await queryBuilder.getManyAndCount()
return createPaginationObject<T>({
items,
totalItems: total,
currentPage: page,
limit,
})
}
export async function paginateRaw<T>(
queryBuilder: SelectQueryBuilder<T>,
options: IPaginationOptions,
): Promise<Pagination<T>> {
const [page, limit, paginationType] = resolveOptions(options)
const promises: [Promise<T[]>, Promise<number> | undefined] = [
(paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET
? queryBuilder.limit(limit).offset((page - 1) * limit)
: queryBuilder.take(limit).skip((page - 1) * limit)
).getRawMany<T>(),
queryBuilder.getCount(),
]
const [items, total] = await Promise.all(promises)
return createPaginationObject<T>({
items,
totalItems: total,
currentPage: page,
limit,
})
}
export async function paginateRawAndEntities<T>(
queryBuilder: SelectQueryBuilder<T>,
options: IPaginationOptions,
): Promise<[Pagination<T>, Partial<T>[]]> {
const [page, limit, paginationType] = resolveOptions(options)
const promises: [
Promise<{ entities: T[], raw: T[] }>,
Promise<number> | undefined,
] = [
(paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET
? queryBuilder.limit(limit).offset((page - 1) * limit)
: queryBuilder.take(limit).skip((page - 1) * limit)
).getRawAndEntities<T>(),
queryBuilder.getCount(),
]
const [itemObject, total] = await Promise.all(promises)
return [
createPaginationObject<T>({
items: itemObject.entities,
totalItems: total,
currentPage: page,
limit,
}),
itemObject.raw,
]
}
export async function paginate<T extends ObjectLiteral>(
repository: Repository<T>,
options: IPaginationOptions,
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>,
): Promise<Pagination<T>>
export async function paginate<T>(
queryBuilder: SelectQueryBuilder<T>,
options: IPaginationOptions,
): Promise<Pagination<T>>
export async function paginate<T extends ObjectLiteral>(
repositoryOrQueryBuilder: Repository<T> | SelectQueryBuilder<T>,
options: IPaginationOptions,
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>,
) {
return repositoryOrQueryBuilder instanceof Repository
? paginateRepository<T>(repositoryOrQueryBuilder, options, searchOptions)
: paginateQueryBuilder<T>(repositoryOrQueryBuilder, options)
}

View File

@ -0,0 +1,27 @@
import { ObjectLiteral } from 'typeorm'
export enum PaginationTypeEnum {
LIMIT_AND_OFFSET = 'limit',
TAKE_AND_SKIP = 'take',
}
export interface IPaginationOptions {
page: number
pageSize: number
paginationType?: PaginationTypeEnum
}
export interface IPaginationMeta extends ObjectLiteral {
itemCount: number
totalItems?: number
itemsPerPage: number
totalPages?: number
currentPage: number
}
export interface IPaginationLinks {
first?: string
previous?: string
next?: string
last?: string
}

View File

@ -0,0 +1,14 @@
import { ObjectLiteral } from 'typeorm'
import { IPaginationMeta } from './interface'
export class Pagination<
PaginationObject,
T extends ObjectLiteral = IPaginationMeta,
> {
constructor(
public readonly items: PaginationObject[],
public readonly meta: T,
) {}
}

101
src/main.ts Normal file
View File

@ -0,0 +1,101 @@
import cluster from 'node:cluster'
import path from 'node:path'
import {
HttpStatus,
Logger,
UnprocessableEntityException,
ValidationPipe,
} from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { NestFactory } from '@nestjs/core'
import { NestFastifyApplication } from '@nestjs/platform-fastify'
import { useContainer } from 'class-validator'
import { AppModule } from './app.module'
import { fastifyApp } from './common/adapters/fastify.adapter'
import { RedisIoAdapter } from './common/adapters/socket.adapter'
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
import type { ConfigKeyPaths } from './config'
import { isDev, isMainProcess } from './global/env'
import { setupSwagger } from './setup-swagger'
import { LoggerService } from './shared/logger/logger.service'
declare const module: any
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
fastifyApp,
{
bufferLogs: true,
snapshot: true,
// forceCloseConnections: true,
},
)
const configService = app.get(ConfigService<ConfigKeyPaths>)
const { port, globalPrefix } = configService.get('app', { infer: true })
// class-validator 的 DTO 类中注入 nest 容器的依赖 (用于自定义验证器)
useContainer(app.select(AppModule), { fallbackOnErrors: true })
app.enableCors({ origin: '*', credentials: true })
app.setGlobalPrefix(globalPrefix)
app.useStaticAssets({ root: path.join(__dirname, '..', 'public') })
// Starts listening for shutdown hooks
!isDev && app.enableShutdownHooks()
if (isDev)
app.useGlobalInterceptors(new LoggingInterceptor())
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
transformOptions: { enableImplicitConversion: true },
// forbidNonWhitelisted: true, // 禁止 无装饰器验证的数据通过
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
stopAtFirstError: true,
exceptionFactory: errors =>
new UnprocessableEntityException(
errors.map((e) => {
const rule = Object.keys(e.constraints!)[0]
const msg = e.constraints![rule]
return msg
})[0],
),
}),
)
app.useWebSocketAdapter(new RedisIoAdapter(app))
setupSwagger(app, configService)
await app.listen(port, '0.0.0.0', async () => {
app.useLogger(app.get(LoggerService))
const url = await app.getUrl()
const { pid } = process
const env = cluster.isPrimary
const prefix = env ? 'P' : 'W'
if (!isMainProcess)
return
const logger = new Logger('NestApplication')
logger.log(`[${prefix + pid}] Server running on ${url}`)
if (isDev)
logger.log(`[${prefix + pid}] OpenAPI: ${url}/api-docs`)
})
if (module.hot) {
module.hot.accept()
module.hot.dispose(() => app.close())
}
}
bootstrap()

View File

@ -0,0 +1,15 @@
import fs from 'node:fs'
import path from 'node:path'
import { MigrationInterface, QueryRunner } from 'typeorm'
const sql = fs.readFileSync(path.join(__dirname, '../../deploy/sql/nest_admin.sql'), 'utf8')
export class InitData1707996695540 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(sql)
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}

View File

@ -0,0 +1,26 @@
export const PUBLIC_KEY = '__public_key__'
export const PERMISSION_KEY = '__permission_key__'
export const RESOURCE_KEY = '__resource_key__'
export const ALLOW_ANON_KEY = '__allow_anon_permission_key__'
export const AuthStrategy = {
LOCAL: 'local',
LOCAL_EMAIL: 'local_email',
LOCAL_PHONE: 'local_phone',
JWT: 'jwt',
GITHUB: 'github',
GOOGLE: 'google',
} as const
export const Roles = {
ADMIN: 'admin',
USER: 'user',
// GUEST: 'guest',
} as const
export type Role = (typeof Roles)[keyof typeof Roles]

View File

@ -0,0 +1,47 @@
import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common'
import { ApiOperation, ApiTags } from '@nestjs/swagger'
import { ApiResult } from '~/common/decorators/api-result.decorator'
import { Ip } from '~/common/decorators/http.decorator'
import { UserService } from '../user/user.service'
import { AuthService } from './auth.service'
import { Public } from './decorators/public.decorator'
import { LoginDto, RegisterDto } from './dto/auth.dto'
import { LocalGuard } from './guards/local.guard'
import { LoginToken } from './models/auth.model'
import { CaptchaService } from './services/captcha.service'
@ApiTags('Auth - 认证模块')
@UseGuards(LocalGuard)
@Public()
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private userService: UserService,
private captchaService: CaptchaService,
) {}
@Post('login')
@ApiOperation({ summary: '登录' })
@ApiResult({ type: LoginToken })
async login(
@Body() dto: LoginDto, @Ip() ip: string, @Headers('user-agent') ua: string): Promise<LoginToken> {
await this.captchaService.checkImgCaptcha(dto.captchaId, dto.verifyCode)
const token = await this.authService.login(
dto.username,
dto.password,
ip,
ua,
)
return { token }
}
@Post('register')
@ApiOperation({ summary: '注册' })
async register(@Body() dto: RegisterDto): Promise<void> {
await this.userService.register(dto)
}
}

View File

@ -0,0 +1,64 @@
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
import { TypeOrmModule } from '@nestjs/typeorm'
import { ConfigKeyPaths, ISecurityConfig } from '~/config'
import { isDev } from '~/global/env'
import { LogModule } from '../system/log/log.module'
import { MenuModule } from '../system/menu/menu.module'
import { RoleModule } from '../system/role/role.module'
import { UserModule } from '../user/user.module'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { AccountController } from './controllers/account.controller'
import { CaptchaController } from './controllers/captcha.controller'
import { EmailController } from './controllers/email.controller'
import { AccessTokenEntity } from './entities/access-token.entity'
import { RefreshTokenEntity } from './entities/refresh-token.entity'
import { CaptchaService } from './services/captcha.service'
import { TokenService } from './services/token.service'
import { JwtStrategy } from './strategies/jwt.strategy'
import { LocalStrategy } from './strategies/local.strategy'
const controllers = [
AuthController,
AccountController,
CaptchaController,
EmailController,
]
const providers = [AuthService, TokenService, CaptchaService]
const strategies = [LocalStrategy, JwtStrategy]
@Module({
imports: [
TypeOrmModule.forFeature([AccessTokenEntity, RefreshTokenEntity]),
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService<ConfigKeyPaths>) => {
const { jwtSecret, jwtExprire }
= configService.get<ISecurityConfig>('security')
return {
secret: jwtSecret,
expires: jwtExprire,
ignoreExpiration: isDev,
}
},
inject: [ConfigService],
}),
UserModule,
RoleModule,
MenuModule,
LogModule,
],
controllers: [...controllers],
providers: [...providers, ...strategies],
exports: [TypeOrmModule, JwtModule, ...providers],
})
export class AuthModule {}

View File

@ -0,0 +1,156 @@
import { InjectRedis } from '@liaoliaots/nestjs-redis'
import { Injectable } from '@nestjs/common'
import Redis from 'ioredis'
import { isEmpty } from 'lodash'
import { BusinessException } from '~/common/exceptions/biz.exception'
import { ErrorEnum } from '~/constants/error-code.constant'
import { genAuthPVKey, genAuthPermKey, genAuthTokenKey } from '~/helper/genRedisKey'
import { UserService } from '~/modules/user/user.service'
import { md5 } from '~/utils'
import { LoginLogService } from '../system/log/services/login-log.service'
import { MenuService } from '../system/menu/menu.service'
import { RoleService } from '../system/role/role.service'
import { TokenService } from './services/token.service'
@Injectable()
export class AuthService {
constructor(
@InjectRedis() private readonly redis: Redis,
private menuService: MenuService,
private roleService: RoleService,
private userService: UserService,
private loginLogService: LoginLogService,
private tokenService: TokenService,
) {}
async validateUser(credential: string, password: string): Promise<any> {
const user = await this.userService.findUserByUserName(credential)
if (isEmpty(user))
throw new BusinessException(ErrorEnum.USER_NOT_FOUND)
const comparePassword = md5(`${password}${user.psalt}`)
if (user.password !== comparePassword)
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
if (user) {
const { password, ...result } = user
return result
}
return null
}
/**
* JWT
* null则账号密码有误
*/
async login(
username: string,
password: string,
ip: string,
ua: string,
): Promise<string> {
const user = await this.userService.findUserByUserName(username)
if (isEmpty(user))
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
const comparePassword = md5(`${password}${user.psalt}`)
if (user.password !== comparePassword)
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
const roleIds = await this.roleService.getRoleIdsByUser(user.id)
const roles = await this.roleService.getRoleValues(roleIds)
// 包含access_token和refresh_token
const token = await this.tokenService.generateAccessToken(user.id, roles)
await this.redis.set(genAuthTokenKey(user.id), token.accessToken)
// 设置密码版本号 当密码修改时,版本号+1
await this.redis.set(genAuthPVKey(user.id), 1)
// 设置菜单权限
const permissions = await this.menuService.getPermissions(user.id)
await this.setPermissionsCache(user.id, permissions)
await this.loginLogService.create(user.id, ip, ua)
return token.accessToken
}
/**
*
*/
async checkPassword(username: string, password: string) {
const user = await this.userService.findUserByUserName(username)
const comparePassword = md5(`${password}${user.psalt}`)
if (user.password !== comparePassword)
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
}
async loginLog(uid: number, ip: string, ua: string) {
await this.loginLogService.create(uid, ip, ua)
}
async logout(uid: number) {
// 删除token
await this.userService.forbidden(uid)
}
/**
*
*/
async resetPassword(username: string, password: string) {
const user = await this.userService.findUserByUserName(username)
await this.userService.forceUpdatePassword(user.id, password)
}
/**
*
*/
async clearLoginStatus(uid: number): Promise<void> {
await this.userService.forbidden(uid)
}
/**
*
*/
async getMenus(uid: number): Promise<string[]> {
return this.menuService.getMenus(uid)
}
/**
*
*/
async getPermissions(uid: number): Promise<string[]> {
return this.menuService.getPermissions(uid)
}
async getPermissionsCache(uid: number): Promise<string[]> {
const permissionString = await this.redis.get(genAuthPermKey(uid))
return permissionString ? JSON.parse(permissionString) : []
}
async setPermissionsCache(uid: number, permissions: string[]): Promise<void> {
await this.redis.set(genAuthPermKey(uid), JSON.stringify(permissions))
}
async getPasswordVersionByUid(uid: number): Promise<string> {
return this.redis.get(genAuthPVKey(uid))
}
async getTokenByUid(uid: number): Promise<string> {
return this.redis.get(genAuthTokenKey(uid))
}
}

View File

@ -0,0 +1,75 @@
import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common'
import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'
import { ApiResult } from '~/common/decorators/api-result.decorator'
import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'
import { AllowAnon } from '~/modules/auth/decorators/allow-anon.decorator'
import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'
import { PasswordUpdateDto } from '~/modules/user/dto/password.dto'
import { AccountInfo } from '../../user/user.model'
import { UserService } from '../../user/user.service'
import { AuthService } from '../auth.service'
import { AccountMenus, AccountUpdateDto } from '../dto/account.dto'
import { JwtAuthGuard } from '../guards/jwt-auth.guard'
@ApiTags('Account - 账户模块')
@ApiSecurityAuth()
@ApiExtraModels(AccountInfo)
@UseGuards(JwtAuthGuard)
@Controller('account')
export class AccountController {
constructor(
private userService: UserService,
private authService: AuthService,
) {}
@Get('profile')
@ApiOperation({ summary: '获取账户资料' })
@ApiResult({ type: AccountInfo })
@AllowAnon()
async profile(@AuthUser() user: IAuthUser): Promise<AccountInfo> {
return this.userService.getAccountInfo(user.uid)
}
@Get('logout')
@ApiOperation({ summary: '账户登出' })
@AllowAnon()
async logout(@AuthUser() user: IAuthUser): Promise<void> {
await this.authService.clearLoginStatus(user.uid)
}
@Get('menus')
@ApiOperation({ summary: '获取菜单列表' })
@ApiResult({ type: [AccountMenus] })
@AllowAnon()
async menu(@AuthUser() user: IAuthUser): Promise<string[]> {
return this.authService.getMenus(user.uid)
}
@Get('permissions')
@ApiOperation({ summary: '获取权限列表' })
@ApiResult({ type: [String] })
@AllowAnon()
async permissions(@AuthUser() user: IAuthUser): Promise<string[]> {
return this.authService.getPermissions(user.uid)
}
@Put('update')
@ApiOperation({ summary: '更改账户资料' })
@AllowAnon()
async update(
@AuthUser() user: IAuthUser, @Body() dto: AccountUpdateDto): Promise<void> {
await this.userService.updateAccountInfo(user.uid, dto)
}
@Post('password')
@ApiOperation({ summary: '更改账户密码' })
@AllowAnon()
async password(
@AuthUser() user: IAuthUser, @Body() dto: PasswordUpdateDto): Promise<void> {
await this.userService.updatePassword(user.uid, dto)
}
}

View File

@ -0,0 +1,50 @@
import { InjectRedis } from '@liaoliaots/nestjs-redis'
import { Controller, Get, Query } from '@nestjs/common'
import { ApiOperation, ApiTags } from '@nestjs/swagger'
import Redis from 'ioredis'
import { isEmpty } from 'lodash'
import * as svgCaptcha from 'svg-captcha'
import { ApiResult } from '~/common/decorators/api-result.decorator'
import { genCaptchaImgKey } from '~/helper/genRedisKey'
import { generateUUID } from '~/utils'
import { Public } from '../decorators/public.decorator'
import { ImageCaptchaDto } from '../dto/captcha.dto'
import { ImageCaptcha } from '../models/auth.model'
@ApiTags('Captcha - 验证码模块')
// @UseGuards(ThrottlerGuard)
@Controller('auth/captcha')
export class CaptchaController {
constructor(@InjectRedis() private redis: Redis) {}
@Get('img')
@ApiOperation({ summary: '获取登录图片验证码' })
@ApiResult({ type: ImageCaptcha })
@Public()
// @Throttle({ default: { limit: 2, ttl: 600000 } })
async captchaByImg(@Query() dto: ImageCaptchaDto): Promise<ImageCaptcha> {
const { width, height } = dto
const svg = svgCaptcha.create({
size: 4,
color: true,
noise: 4,
width: isEmpty(width) ? 100 : width,
height: isEmpty(height) ? 50 : height,
charPreset: '1234567890',
})
const result = {
img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString(
'base64',
)}`,
id: generateUUID(),
}
// 5分钟过期时间
await this.redis.set(genCaptchaImgKey(result.id), svg.text, 'EX', 60 * 5)
return result
}
}

View File

@ -0,0 +1,41 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common'
import { ApiOperation, ApiTags } from '@nestjs/swagger'
import { Throttle, ThrottlerGuard } from '@nestjs/throttler'
import { Ip } from '~/common/decorators/http.decorator'
import { MailerService } from '~/shared/mailer/mailer.service'
import { Public } from '../decorators/public.decorator'
import { SendEmailCodeDto } from '../dto/captcha.dto'
@ApiTags('Auth - 认证模块')
@UseGuards(ThrottlerGuard)
@Controller('auth/email')
export class EmailController {
constructor(private mailerService: MailerService) {}
@Post('send')
@ApiOperation({ summary: '发送邮箱验证码' })
@Public()
@Throttle({ default: { limit: 2, ttl: 600000 } })
async sendEmailCode(
@Body() dto: SendEmailCodeDto,
@Ip() ip: string,
): Promise<void> {
// await this.authService.checkImgCaptcha(dto.captchaId, dto.verifyCode);
const { email } = dto
await this.mailerService.checkLimit(email, ip)
const { code } = await this.mailerService.sendVerificationCode(email)
await this.mailerService.log(email, code, ip)
}
// @Post()
// async authWithEmail(@AuthUser() user: IAuthUser) {
// // TODO:
// }
}

View File

@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common'
import { ALLOW_ANON_KEY } from '../auth.constant'
/**
*
*/
export const AllowAnon = () => SetMetadata(ALLOW_ANON_KEY, true)

View File

@ -0,0 +1,17 @@
import { ExecutionContext, createParamDecorator } from '@nestjs/common'
import { FastifyRequest } from 'fastify'
type Payload = keyof IAuthUser
/**
* @description , request上
*/
export const AuthUser = createParamDecorator(
(data: Payload, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest<FastifyRequest>()
// auth guard will mount this
const user = request.user as IAuthUser
return data ? user?.[data] : user
},
)

View File

@ -0,0 +1,58 @@
import { SetMetadata, applyDecorators } from '@nestjs/common'
import { isPlainObject } from 'lodash'
import { PERMISSION_KEY } from '../auth.constant'
type TupleToObject<T extends string, P extends ReadonlyArray<string>> = {
[K in Uppercase<P[number]>]: `${T}:${Lowercase<K>}`
}
type AddPrefixToObjectValue<T extends string, P extends Record<string, string>> = {
[K in keyof P]: K extends string ? `${T}:${P[K]}` : never
}
/** 资源操作需要特定的权限 */
export function Perm(permission: string | string[]) {
return applyDecorators(SetMetadata(PERMISSION_KEY, permission))
}
/** (此举非必需)保存通过 definePermission 定义的所有权限,可用于前端开发人员开发阶段的 ts 类型提示,避免前端权限定义与后端定义不匹配 */
let permissions: string[] = []
/**
*
*
* - , eg:
* ```ts
* definePermission('app:health', {
* NETWORK: 'network'
* };
* ```
*
* - , eg:
* ```ts
* definePermission('app:health', ['network']);
* ```
*/
export function definePermission<T extends string, U extends Record<string, string>>(modulePrefix: T, actionMap: U): AddPrefixToObjectValue<T, U>
export function definePermission<T extends string, U extends ReadonlyArray<string>>(modulePrefix: T, actions: U): TupleToObject<T, U>
export function definePermission(modulePrefix: string, actions) {
if (isPlainObject(actions)) {
Object.entries(actions).forEach(([key, action]) => {
actions[key] = `${modulePrefix}:${action}`
})
permissions = [...new Set([...permissions, ...Object.values<string>(actions)])]
return actions
}
else if (Array.isArray(actions)) {
const permissionFormats = actions.map(action => `${modulePrefix}:${action}`)
permissions = [...new Set([...permissions, ...permissionFormats])]
return actions.reduce((prev, action) => {
prev[action.toUpperCase()] = `${modulePrefix}:${action}`
return prev
}, {})
}
}
/** 获取所有通过 definePermission 定义的权限 */
export const getDefinePermissions = () => permissions

View File

@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common'
import { PUBLIC_KEY } from '../auth.constant'
/**
*
*/
export const Public = () => SetMetadata(PUBLIC_KEY, true)

View File

@ -0,0 +1,12 @@
import { SetMetadata, applyDecorators } from '@nestjs/common'
import { ObjectLiteral, ObjectType, Repository } from 'typeorm'
import { RESOURCE_KEY } from '../auth.constant'
export type Condition<E extends ObjectLiteral = any> = (Repository: Repository<E>, items: number[], user: IAuthUser) => Promise<boolean>
export interface ResourceObject { entity: ObjectType<any>, condition: Condition }
export function Resource<E extends ObjectLiteral = any>(entity: ObjectType<E>, condition?: Condition<E>) {
return applyDecorators(SetMetadata(RESOURCE_KEY, { entity, condition }))
}

View File

@ -0,0 +1,64 @@
import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger'
import {
IsEmail,
IsOptional,
IsString,
Matches,
MaxLength,
MinLength,
} from 'class-validator'
import { MenuEntity } from '~/modules/system/menu/menu.entity'
export class AccountUpdateDto {
@ApiProperty({ description: '用户呢称' })
@IsString()
@IsOptional()
nickname: string
@ApiProperty({ description: '用户邮箱' })
@IsEmail()
email: string
@ApiProperty({ description: '用户QQ' })
@IsOptional()
@IsString()
@Matches(/^[0-9]+$/)
@MinLength(5)
@MaxLength(11)
qq: string
@ApiProperty({ description: '用户手机号' })
@IsOptional()
@IsString()
phone: string
@ApiProperty({ description: '用户头像' })
@IsOptional()
@IsString()
avatar: string
@ApiProperty({ description: '用户备注' })
@IsOptional()
@IsString()
remark: string
}
export class ResetPasswordDto {
@ApiProperty({ description: '临时token', example: 'uuid' })
@IsString()
accessToken: string
@ApiProperty({ description: '密码', example: 'a123456' })
@IsString()
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
@MinLength(6)
password: string
}
export class MenuMeta extends PartialType(OmitType(MenuEntity, ['parentId', 'createdAt', 'updatedAt', 'id', 'roles', 'path', 'name'] as const)) {
title: string
}
export class AccountMenus extends PickType(MenuEntity, ['id', 'path', 'name', 'component'] as const) {
meta: MenuMeta
}

View File

@ -0,0 +1,43 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString, Matches, MaxLength, MinLength } from 'class-validator'
export class LoginDto {
@ApiProperty({ description: '手机号/邮箱' })
@IsString()
@MinLength(4)
username: string
@ApiProperty({ description: '密码', example: 'a123456' })
@IsString()
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
@MinLength(6)
password: string
@ApiProperty({ description: '验证码标识' })
@IsString()
captchaId: string
@ApiProperty({ description: '用户输入的验证码' })
@IsString()
@MinLength(4)
@MaxLength(4)
verifyCode: string
}
export class RegisterDto {
@ApiProperty({ description: '账号' })
@IsString()
username: string
@ApiProperty({ description: '密码' })
@IsString()
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
@MinLength(6)
@MaxLength(16)
password: string
@ApiProperty({ description: '语言', examples: ['EN', 'ZH'] })
@IsString()
lang: string
}

View File

@ -0,0 +1,53 @@
import { ApiProperty } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import {
IsEmail,
IsInt,
IsMobilePhone,
IsOptional,
IsString,
} 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 SendEmailCodeDto {
@ApiProperty({ description: '邮箱' })
@IsEmail({}, { message: '邮箱格式不正确' })
email: string
}
export class SendSmsCodeDto {
@ApiProperty({ description: '手机号' })
@IsMobilePhone('zh-CN', {}, { message: '手机号格式不正确' })
phone: string
}
export class CheckCodeDto {
@ApiProperty({ description: '手机号/邮箱' })
@IsString()
account: string
@ApiProperty({ description: '验证码' })
@IsString()
code: string
}

View File

@ -0,0 +1,40 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm'
import { UserEntity } from '~/modules/user/user.entity'
import { RefreshTokenEntity } from './refresh-token.entity'
@Entity('user_access_tokens')
export class AccessTokenEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string
@Column({ length: 500 })
value!: string
@Column({ comment: '令牌过期时间' })
expired_at!: Date
@CreateDateColumn({ comment: '令牌创建时间' })
created_at!: Date
@OneToOne(() => RefreshTokenEntity, refreshToken => refreshToken.accessToken, {
cascade: true,
})
refreshToken!: RefreshTokenEntity
@ManyToOne(() => UserEntity, user => user.accessTokens, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'user_id' })
user!: UserEntity
}

View File

@ -0,0 +1,32 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm'
import { AccessTokenEntity } from './access-token.entity'
@Entity('user_refresh_tokens')
export class RefreshTokenEntity extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string
@Column({ length: 500 })
value!: string
@Column({ comment: '令牌过期时间' })
expired_at!: Date
@CreateDateColumn({ comment: '令牌创建时间' })
created_at!: Date
@OneToOne(() => AccessTokenEntity, accessToken => accessToken.refreshToken, {
onDelete: 'CASCADE',
})
@JoinColumn()
accessToken!: AccessTokenEntity
}

Some files were not shown because too many files have changed in this diff Show More