feat: first commit
This commit is contained in:
1
server/.env
Normal file
1
server/.env
Normal file
@@ -0,0 +1 @@
|
||||
DATABASE_URL="postgresql://postgres:ultimate@81.70.149.52:8000/promise?schema=public"
|
||||
32
server/.eslintrc.js
Normal file
32
server/.eslintrc.js
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir : __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"endOfLine": "auto"
|
||||
}
|
||||
]
|
||||
},
|
||||
};
|
||||
35
server/.gitignore
vendored
Normal file
35
server/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
4
server/.prettierrc
Normal file
4
server/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
4
server/README.md
Normal file
4
server/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
```shell
|
||||
# 种子文件
|
||||
npx prisma db seed
|
||||
```
|
||||
9
server/nest-cli.json
Normal file
9
server/nest-cli.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"assets": ["**/*.yaml"],
|
||||
"watchAssets": true
|
||||
}
|
||||
}
|
||||
102
server/package.json
Normal file
102
server/package.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"prebuild": "rimraf dist",
|
||||
"build": "cross-env NODE_ENV=production nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"dev": "cross-env NODE_ENV=development nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
"@nestjs/common": "^10.2.7",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.2.7",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-express": "^10.2.7",
|
||||
"@prisma/client": "5.5.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cache-manager": "^5.5.3",
|
||||
"cache-manager-redis-store": "^3.0.1",
|
||||
"cache-manager-redis-yet": "^5.0.1",
|
||||
"chalk": "^4.1.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"minio": "^8.0.1",
|
||||
"nanoid": "^3.3.6",
|
||||
"nest-winston": "^1.9.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^7.2.0",
|
||||
"winston": "^3.9.0",
|
||||
"winston-daily-rotate-file": "^4.7.1"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "ts-node prisma/seed.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.2.0",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/testing": "^10.2.7",
|
||||
"@types/cache-manager-redis-store": "^2.0.4",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "27.5.0",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/mime-types": "^2.1.3",
|
||||
"@types/multer": "^1.4.9",
|
||||
"@types/node": "^16.18.101",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "28.0.3",
|
||||
"prettier": "^2.3.2",
|
||||
"prisma": "5.5.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "28.0.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "4.0.0",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.20.4"
|
||||
}
|
||||
}
|
||||
117
server/prisma/migrations/20241026034430_/migration.sql
Normal file
117
server/prisma/migrations/20241026034430_/migration.sql
Normal file
@@ -0,0 +1,117 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_user" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT,
|
||||
"account" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"password_salt" TEXT NOT NULL,
|
||||
"status" SMALLINT NOT NULL DEFAULT 0,
|
||||
"avatar_id" INTEGER,
|
||||
"gender" SMALLINT NOT NULL DEFAULT 0,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "sys_user_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_role" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"code" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"status" SMALLINT NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "sys_role_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_user_role" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"role_id" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "sys_user_role_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "files" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "files_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_permission" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"type" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "sys_permission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_menu" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"parent_id" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "sys_menu_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_menu_permission" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"menu_id" INTEGER NOT NULL,
|
||||
"permission_id" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "sys_menu_permission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_role_permission" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"role_id" INTEGER NOT NULL,
|
||||
"permission_id" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "sys_role_permission_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sys_config" (
|
||||
"key" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
"remark" TEXT
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sys_user_account_key" ON "sys_user"("account");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sys_user_avatar_id_idx" ON "sys_user"("avatar_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sys_role_code_key" ON "sys_role"("code");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sys_user_role_user_id_idx" ON "sys_user_role"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sys_user_role_role_id_idx" ON "sys_user_role"("role_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sys_menu_permission_menu_id_idx" ON "sys_menu_permission"("menu_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sys_menu_permission_permission_id_idx" ON "sys_menu_permission"("permission_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sys_role_permission_role_id_idx" ON "sys_role_permission"("role_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "sys_role_permission_permission_id_idx" ON "sys_role_permission"("permission_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "sys_config_key_key" ON "sys_config"("key");
|
||||
3
server/prisma/migrations/migration_lock.toml
Normal file
3
server/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
123
server/prisma/schema.prisma
Normal file
123
server/prisma/schema.prisma
Normal file
@@ -0,0 +1,123 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
relationMode = "prisma"
|
||||
}
|
||||
|
||||
// 系统用户表
|
||||
model SysUser {
|
||||
id Int @id @default(autoincrement()) /// 主键id
|
||||
name String? /// 用户姓名
|
||||
account String @unique /// 账户
|
||||
password String /// 密码
|
||||
passwordSalt String @map("password_salt") /// 密码盐
|
||||
status Int @default(0) @db.SmallInt /// 用户状态
|
||||
avatarId Int? @map("avatar_id") /// 用户头像id
|
||||
avatar Files? @relation(fields: [avatarId], references: [id])
|
||||
gender Int @default(0) @db.SmallInt /// 用户性别
|
||||
createdAt DateTime @default(now()) @map("created_at") /// 创建时间
|
||||
updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间
|
||||
userRole SysUserRole[]
|
||||
|
||||
@@index([avatarId])
|
||||
@@map("sys_user")
|
||||
}
|
||||
|
||||
// 系统角色表
|
||||
model SysRole {
|
||||
id Int @id @default(autoincrement())
|
||||
code String @unique
|
||||
name String
|
||||
status Int @default(0) @db.SmallInt /// 角色状态
|
||||
sysRolePermission SysRolePermission[]
|
||||
sysUserRole SysUserRole[]
|
||||
|
||||
@@map("sys_role")
|
||||
}
|
||||
|
||||
// 用户角色关联表
|
||||
model SysUserRole {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @map("user_id")
|
||||
roleId Int @map("role_id")
|
||||
user SysUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
role SysRole @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([roleId])
|
||||
@@map("sys_user_role")
|
||||
}
|
||||
|
||||
// 文件表
|
||||
model Files {
|
||||
id Int @id @default(autoincrement())
|
||||
name String /// 文件名称
|
||||
path String
|
||||
createdAt DateTime @default(now()) @map("created_at") /// 创建时间
|
||||
sysUser SysUser[]
|
||||
|
||||
@@map("files")
|
||||
}
|
||||
|
||||
// 权限表
|
||||
model SysPermission {
|
||||
id Int @id @default(autoincrement())
|
||||
type Int
|
||||
sysRolePermission SysRolePermission[]
|
||||
sysMenuPermission SysMenuPermission[]
|
||||
|
||||
@@map("sys_permission")
|
||||
}
|
||||
|
||||
// 系统菜单表
|
||||
model SysMenu {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
url String
|
||||
parentId Int @default(0) @map("parent_id")
|
||||
sysMenuPermission SysMenuPermission[]
|
||||
|
||||
@@map("sys_menu")
|
||||
}
|
||||
|
||||
// 菜单权限关联表
|
||||
model SysMenuPermission {
|
||||
id Int @id @default(autoincrement())
|
||||
menuId Int @map("menu_id")
|
||||
permissionId Int @map("permission_id")
|
||||
menu SysMenu @relation(fields: [menuId], references: [id], onDelete: Cascade)
|
||||
permission SysPermission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([menuId])
|
||||
@@index([permissionId])
|
||||
@@map("sys_menu_permission")
|
||||
}
|
||||
|
||||
// 角色权限关联表
|
||||
model SysRolePermission {
|
||||
id Int @id @default(autoincrement())
|
||||
roleId Int @map("role_id")
|
||||
permissionId Int @map("permission_id")
|
||||
role SysRole @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
||||
permission SysPermission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([roleId])
|
||||
@@index([permissionId])
|
||||
@@map("sys_role_permission")
|
||||
}
|
||||
|
||||
// 系统配置
|
||||
model SysConfig {
|
||||
key String @unique
|
||||
value String
|
||||
remark String?
|
||||
|
||||
@@map("sys_config")
|
||||
}
|
||||
51
server/prisma/seed.ts
Normal file
51
server/prisma/seed.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
enum PermissionType {
|
||||
Menu,
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await prisma.sysUser.upsert({
|
||||
where: { account: 'admin' },
|
||||
update: {},
|
||||
create: {
|
||||
name: '管理员',
|
||||
account: 'admin',
|
||||
password: '$2a$10$rWb52cHK1hrWuQQg3LiOJ.crDfIqjoOIoglGM96a.Ao1DL/dQwKCu',
|
||||
passwordSalt: '$2a$10$rWb52cHK1hrWuQQg3LiOJ.',
|
||||
status: 1,
|
||||
},
|
||||
});
|
||||
const menus = [
|
||||
{ name: '用户管理', url: 'user', parentId: 0 },
|
||||
{ name: '角色管理', url: 'role', parentId: 0 },
|
||||
{ name: '菜单管理', url: 'menu', parentId: 0 },
|
||||
];
|
||||
|
||||
for (const menu of menus) {
|
||||
// 创建菜单
|
||||
await prisma.$transaction(async (_prisma) => {
|
||||
const { id: menuId } = await _prisma.sysMenu.create({ data: menu });
|
||||
// 创建菜单权限
|
||||
const { id: permissionId } = await _prisma.sysPermission.create({
|
||||
data: { type: PermissionType.Menu },
|
||||
});
|
||||
// 关联权限和菜单
|
||||
await _prisma.sysMenuPermission.create({
|
||||
data: { menuId, permissionId },
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.then(async () => {
|
||||
console.log('seed done!');
|
||||
await prisma.$disconnect();
|
||||
})
|
||||
.catch(async (e) => {
|
||||
console.error(e);
|
||||
await prisma.$disconnect();
|
||||
process.exit(1);
|
||||
});
|
||||
45
server/src/app.module.ts
Normal file
45
server/src/app.module.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import configuration from './config/configuration';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { AuthGuard } from './common/guard/auth.guard';
|
||||
import { PermissionGuard } from '@/common/guard/permission.guard';
|
||||
import { SystemModule } from './system/system.module';
|
||||
import { PrismaModule } from '@/prisma/prisma.module';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { redisStore } from 'cache-manager-redis-yet';
|
||||
import { MinioService } from './minio/minio.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
}),
|
||||
PrismaModule,
|
||||
// 系统模块
|
||||
SystemModule,
|
||||
// https://github.com/dabroek/node-cache-manager-redis-store/issues/40
|
||||
CacheModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
isGlobal: true,
|
||||
useFactory: async (config: ConfigService) => ({
|
||||
store: await redisStore({
|
||||
ttl: config.get('redis.ttl'),
|
||||
socket: {
|
||||
host: config.get('redis.host'),
|
||||
port: config.get('redis.port'),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
{ provide: APP_GUARD, useClass: AuthGuard },
|
||||
{ provide: APP_GUARD, useClass: PermissionGuard },
|
||||
MinioService,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
5
server/src/common/decorators/roles.decorator.ts
Normal file
5
server/src/common/decorators/roles.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Role } from '../enum';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||
5
server/src/common/decorators/skip-auth.decorator.ts
Normal file
5
server/src/common/decorators/skip-auth.decorator.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const SKIP_AUTH = 'SKIP_AUTH';
|
||||
|
||||
export const SkipAuth = () => SetMetadata(SKIP_AUTH, true);
|
||||
13
server/src/common/dto/pagination.dto.ts
Normal file
13
server/src/common/dto/pagination.dto.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IsOptional, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class PaginationDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@Min(1)
|
||||
public readonly page?: number = 1;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
public readonly pageSize?: number = 10;
|
||||
}
|
||||
13
server/src/common/enum/index.ts
Normal file
13
server/src/common/enum/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export enum Role {
|
||||
User = 'user',
|
||||
Admin = 'admin',
|
||||
}
|
||||
|
||||
export enum RedisGroup {
|
||||
Config = 'config',
|
||||
JwtJti = 'jwt_jti',
|
||||
}
|
||||
|
||||
export enum PermissionType {
|
||||
Menu,
|
||||
}
|
||||
65
server/src/common/guard/auth.guard.ts
Normal file
65
server/src/common/guard/auth.guard.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Inject,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { SKIP_AUTH } from '@/common/decorators/skip-auth.decorator';
|
||||
import { RedisGroup } from '@/common/enum';
|
||||
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private readonly configService: ConfigService,
|
||||
private reflector: Reflector,
|
||||
@Inject(CACHE_MANAGER) private cacheService: Cache,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const isSkipAuth = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (isSkipAuth) {
|
||||
request.isSkipAuth = SKIP_AUTH;
|
||||
return true;
|
||||
}
|
||||
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('用户未登录!');
|
||||
}
|
||||
try {
|
||||
const user = await this.jwtService.verifyAsync(token, {
|
||||
secret: this.configService.get('jwt.secret'),
|
||||
});
|
||||
|
||||
const isTokenPresent = await this.cacheService.get(
|
||||
`${RedisGroup.JwtJti}:${user.sub}:${user.jti}`,
|
||||
);
|
||||
|
||||
if (!isTokenPresent) {
|
||||
throw new UnauthorizedException('登录已失效,请重新登录!');
|
||||
}
|
||||
|
||||
request['user'] = user;
|
||||
} catch {
|
||||
throw new UnauthorizedException('登录已失效,请重新登录!');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
33
server/src/common/guard/permission.guard.ts
Normal file
33
server/src/common/guard/permission.guard.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Inject,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { SKIP_AUTH } from '@/common/decorators/skip-auth.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private reflector: Reflector,
|
||||
@Inject(CACHE_MANAGER) private cacheService: Cache,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
request['isAdmin'] = request?.user?.account === 'admin';
|
||||
return true;
|
||||
// if (request.isSkipAuth === SKIP_AUTH || request.user.account === 'admin') {
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// throw new ForbiddenException('无权访问!');
|
||||
}
|
||||
}
|
||||
43
server/src/common/http-exception.filter.ts
Normal file
43
server/src/common/http-exception.filter.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
Logger,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { inspect } from 'util';
|
||||
import { HttpExceptionBody } from '@nestjs/common/interfaces/http/http-exception-body.interface';
|
||||
import maskSensitiveData from '@/common/utils/maskSensitiveData';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
private logger = new Logger('HttpError');
|
||||
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
|
||||
const response = ctx.getResponse<Response>();
|
||||
const { method, url, body, query, headers, socket } =
|
||||
ctx.getRequest<Request>();
|
||||
const exceptionResponse = exception.getResponse() as HttpExceptionBody;
|
||||
const message = exceptionResponse?.message;
|
||||
|
||||
const status = exception.getStatus();
|
||||
const reason = {
|
||||
method,
|
||||
code: status,
|
||||
url,
|
||||
message,
|
||||
};
|
||||
const log = {
|
||||
...reason,
|
||||
body: maskSensitiveData(body),
|
||||
query,
|
||||
clientIp: headers['x-forwarded-for'] || socket.remoteAddress,
|
||||
};
|
||||
this.logger.error(inspect(log, { breakLength: 9999 }));
|
||||
response.status(status).json(reason);
|
||||
}
|
||||
}
|
||||
44
server/src/common/http.interceptor.ts
Normal file
44
server/src/common/http.interceptor.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { inspect } from 'util';
|
||||
import { Request } from 'express';
|
||||
import maskSensitiveData from '@/common/utils/maskSensitiveData';
|
||||
|
||||
@Injectable()
|
||||
export class HttpInterceptor implements NestInterceptor {
|
||||
private logger = new Logger('HttpSuccess');
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const ctx = context.switchToHttp();
|
||||
const { method, body, query, url, headers, socket } =
|
||||
ctx.getRequest<Request>();
|
||||
const { statusCode } = ctx.getResponse();
|
||||
const clientIp = headers['x-forwarded-for'] || socket.remoteAddress;
|
||||
const log = {
|
||||
method,
|
||||
code: statusCode,
|
||||
url,
|
||||
body: maskSensitiveData(body),
|
||||
query,
|
||||
clientIp,
|
||||
};
|
||||
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
this.logger.log(inspect(log, { breakLength: 9999 }));
|
||||
return {
|
||||
code: statusCode,
|
||||
message: '请求成功!',
|
||||
data: data,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
43
server/src/common/utils/maskSensitiveData.ts
Normal file
43
server/src/common/utils/maskSensitiveData.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { cloneDeep, has } from 'lodash';
|
||||
|
||||
// 需要脱敏的字段
|
||||
const fieldList = [
|
||||
{
|
||||
field: 'account',
|
||||
pattern: /^(.).+(.)$/,
|
||||
replacement: '$1***$2',
|
||||
},
|
||||
{
|
||||
field: 'password',
|
||||
pattern: /.*/,
|
||||
replacement: '******',
|
||||
},
|
||||
{
|
||||
field: 'newPassword',
|
||||
pattern: /.*/,
|
||||
replacement: '******',
|
||||
},
|
||||
];
|
||||
|
||||
export default function maskSensitiveData(body: any) {
|
||||
const obj = cloneDeep(body);
|
||||
|
||||
if (typeof obj !== 'object' && obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => maskSensitiveData(item));
|
||||
} else {
|
||||
for (const { field, pattern, replacement } of fieldList) {
|
||||
if (obj[field]) {
|
||||
if (typeof obj[field] === 'string') {
|
||||
obj[field] = obj[field].replace(pattern, replacement);
|
||||
} else {
|
||||
obj[field] = maskSensitiveData(obj[field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
36
server/src/common/validator/custom-starts-with.ts
Normal file
36
server/src/common/validator/custom-starts-with.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
ValidationArguments,
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
} from 'class-validator';
|
||||
|
||||
@ValidatorConstraint({ name: 'customerStartsWith', async: false })
|
||||
export class CustomStartsWith implements ValidatorConstraintInterface {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
const [prefix] = args.constraints;
|
||||
if (typeof value !== 'string') return false;
|
||||
return value.startsWith(prefix);
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
const [prefix] = args.constraints;
|
||||
return `Field $property must start with "${prefix}"`;
|
||||
}
|
||||
}
|
||||
|
||||
export function StartsWithPrefix(
|
||||
prefix: string,
|
||||
validationOptions?: ValidationOptions,
|
||||
) {
|
||||
return function (object: { constructor: any }, propertyName: string) {
|
||||
registerDecorator({
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
constraints: [prefix],
|
||||
validator: CustomStartsWith,
|
||||
});
|
||||
};
|
||||
}
|
||||
21
server/src/config/configuration.ts
Normal file
21
server/src/config/configuration.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export default () => ({
|
||||
jwt: {
|
||||
secret: 'ultimate',
|
||||
expiresIn: '7d',
|
||||
redisExpiresIn: 7 * 60 * 60 * 24 * 1000,
|
||||
},
|
||||
redis: {
|
||||
host: '81.70.149.52',
|
||||
port: 8002,
|
||||
// 默认过期时间 5s
|
||||
ttl: 5000,
|
||||
},
|
||||
minio: {
|
||||
endPoint: 's3.ulti42.com',
|
||||
port: 443,
|
||||
useSSL: true,
|
||||
accessKey: 'vjr5fW3Rk6W0WydqOpCt',
|
||||
secretKey: '3sNkhS5zVf9tTXb3doSN2iJ3NO6WG8alJPoj1Pzd',
|
||||
bucketName: 'static',
|
||||
},
|
||||
});
|
||||
79
server/src/main.ts
Normal file
79
server/src/main.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma/prisma.service';
|
||||
import { ConfigService } from './system/config/config.service';
|
||||
import { HttpExceptionFilter } from 'src/common/http-exception.filter';
|
||||
import { HttpInterceptor } from 'src/common/http.interceptor';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
import { transports, format } from 'winston';
|
||||
import 'winston-daily-rotate-file';
|
||||
|
||||
async function bootstrap() {
|
||||
const logFormat = format.combine(
|
||||
format.timestamp(),
|
||||
format.printf((info) => {
|
||||
return `${info.timestamp} ${info.level} ${info.context} ${info.message}`;
|
||||
}),
|
||||
);
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: WinstonModule.createLogger({
|
||||
transports: [
|
||||
// 记录错误日志
|
||||
new transports.DailyRotateFile({
|
||||
filename: `logs/%DATE%/error.log`,
|
||||
level: 'error',
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: '30d',
|
||||
format: logFormat,
|
||||
}),
|
||||
// logging all level
|
||||
new transports.DailyRotateFile({
|
||||
filename: `logs/%DATE%/combined.log`,
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
maxFiles: '30d',
|
||||
format: logFormat,
|
||||
}),
|
||||
// 控制台输出
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
format.colorize({ all: true }),
|
||||
logFormat,
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
transform: true,
|
||||
whitelist: true,
|
||||
}),
|
||||
);
|
||||
// 全局异常过滤器
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalInterceptors(new HttpInterceptor());
|
||||
|
||||
// prisma
|
||||
const prismaService = app.get(PrismaService);
|
||||
await prismaService.enableShutdownHooks(app);
|
||||
|
||||
// 同步配置到redis
|
||||
const configService = app.get(ConfigService);
|
||||
await configService.syncConfig();
|
||||
|
||||
// 跨域
|
||||
app.enableCors();
|
||||
// 设置全局前缀
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
const logger = new Logger('Server Running');
|
||||
await app.listen(3000, '0.0.0.0', async () => {
|
||||
logger.log(`Application is running on: ${await app.getUrl()}`);
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
41
server/src/minio/minio.service.ts
Normal file
41
server/src/minio/minio.service.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as Minio from 'minio';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class MinioService {
|
||||
private readonly minioClient: Minio.Client;
|
||||
private readonly minioConfig: string;
|
||||
private readonly bucketName: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const { bucketName, ...rest } = this.configService.get('minio');
|
||||
this.minioConfig = rest;
|
||||
this.bucketName = bucketName;
|
||||
this.minioClient = new Minio.Client({
|
||||
...rest,
|
||||
});
|
||||
}
|
||||
|
||||
async createBucketIfNotExists() {
|
||||
const bucketExists = await this.minioClient.bucketExists(this.bucketName);
|
||||
if (!bucketExists) {
|
||||
await this.minioClient.makeBucket(this.bucketName, 'cn-north-1');
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(file: Express.Multer.File, fileName: string) {
|
||||
await this.minioClient.putObject(
|
||||
this.bucketName,
|
||||
fileName,
|
||||
file.buffer,
|
||||
file.size,
|
||||
{ 'Content-Type': file.mimetype },
|
||||
);
|
||||
return `https://${this.minioConfig['endPoint']}/${this.bucketName}/${fileName}`;
|
||||
}
|
||||
|
||||
async deleteFile(fileName: string) {
|
||||
await this.minioClient.removeObject(this.bucketName, fileName);
|
||||
}
|
||||
}
|
||||
9
server/src/prisma/prisma.module.ts
Normal file
9
server/src/prisma/prisma.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
25
server/src/prisma/prisma.service.ts
Normal file
25
server/src/prisma/prisma.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import process from 'process';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
constructor(private reflector: Reflector) {
|
||||
// super({
|
||||
// log: ['query', 'info', 'warn', 'error'],
|
||||
// errorFormat: 'colorless',
|
||||
// });
|
||||
super();
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async enableShutdownHooks(app: INestApplication) {
|
||||
process.on('beforeExit', async () => {
|
||||
await app.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
30
server/src/system/auth/auth.controller.ts
Normal file
30
server/src/system/auth/auth.controller.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Controller, Post, Body, Param } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthDto } from './dto/auth.dto';
|
||||
import { SkipAuth } from '@/common/decorators/skip-auth.decorator';
|
||||
import { UserRoleDto } from './dto/user-role.dto';
|
||||
import { RolePermissionDto } from './dto/role-permission.dto';
|
||||
|
||||
@Controller()
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
// 用户登录
|
||||
@Post('login')
|
||||
@SkipAuth()
|
||||
signIn(@Body() authDto: AuthDto) {
|
||||
return this.authService.signIn(authDto);
|
||||
}
|
||||
|
||||
// 授权用户角色
|
||||
@Post('userRole/:id')
|
||||
grantUserRole(@Param('id') id: string, @Body() dto: UserRoleDto) {
|
||||
return this.authService.grantUserRole(+id, dto);
|
||||
}
|
||||
|
||||
// 授权角色权限
|
||||
@Post('rolePermission/:id')
|
||||
grantRolePermission(@Param('id') id: string, @Body() dto: RolePermissionDto) {
|
||||
return this.authService.grantRolePermission(+id, dto);
|
||||
}
|
||||
}
|
||||
26
server/src/system/auth/auth.module.ts
Normal file
26
server/src/system/auth/auth.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
global: true,
|
||||
useFactory: async (config: ConfigService) => {
|
||||
return {
|
||||
secret: config.get('jwt.secret'),
|
||||
signOptions: { expiresIn: config.get('jwt.expiresIn') },
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
136
server/src/system/auth/auth.service.ts
Normal file
136
server/src/system/auth/auth.service.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthDto } from './dto/auth.dto';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
import { UserRoleDto } from './dto/user-role.dto';
|
||||
import { RolePermissionDto } from './dto/role-permission.dto';
|
||||
import { RedisGroup } from '@/common/enum';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly configService: ConfigService,
|
||||
@Inject(CACHE_MANAGER) private cacheService: Cache,
|
||||
) {}
|
||||
|
||||
// 登录
|
||||
async signIn({ account, password }: AuthDto) {
|
||||
const user = await this.prisma.sysUser.findUnique({
|
||||
where: {
|
||||
account,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('账号或密码错误!');
|
||||
}
|
||||
|
||||
const pass = await bcrypt.hash(password, user.passwordSalt);
|
||||
if (user?.password !== pass) {
|
||||
throw new UnauthorizedException('账号或密码错误!');
|
||||
}
|
||||
|
||||
if (user.status === 0) {
|
||||
throw new ForbiddenException('此账户已被禁用!');
|
||||
}
|
||||
|
||||
const jti = nanoid();
|
||||
|
||||
const payload = { sub: user.id, account: user.account, jti };
|
||||
|
||||
const accessToken = await this.jwtService.signAsync(payload);
|
||||
|
||||
await this.cacheService.set(
|
||||
`${RedisGroup.JwtJti}:${user.id}:${jti}`,
|
||||
user.id,
|
||||
this.configService.get('jwt.redisExpiresIn'),
|
||||
);
|
||||
|
||||
return { accessToken };
|
||||
}
|
||||
|
||||
async grantUserRole(id: number, { roleIds }: UserRoleDto) {
|
||||
const user = await this.prisma.sysUser.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (!user) throw new HttpException('用户不存在!', HttpStatus.NOT_FOUND);
|
||||
|
||||
// 查询所有角色id
|
||||
const allRoles = await this.prisma.sysRole.findMany({
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const allRoleIds = allRoles.map((item) => item.id);
|
||||
|
||||
const isContain = roleIds.every((item) => allRoleIds.includes(item));
|
||||
|
||||
if (!isContain)
|
||||
throw new HttpException(
|
||||
'出现不存在的角色id,请检查传入的roleIds!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
|
||||
const data = roleIds.map((item) => ({
|
||||
userId: id,
|
||||
roleId: item,
|
||||
}));
|
||||
|
||||
await this.prisma.$transaction(async (prisma) => {
|
||||
await prisma.sysUserRole.deleteMany({
|
||||
where: { userId: id },
|
||||
});
|
||||
await prisma.sysUserRole.createMany({ data });
|
||||
});
|
||||
}
|
||||
|
||||
async grantRolePermission(id: number, { permissionIds }: RolePermissionDto) {
|
||||
const role = await this.prisma.sysRole.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
if (!role)
|
||||
throw new HttpException('角色信息不存在!', HttpStatus.NOT_FOUND);
|
||||
|
||||
// 查询所有权限
|
||||
const allPermission = await this.prisma.sysPermission.findMany({
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
const allPermissionIds = allPermission.map((item) => item.id);
|
||||
|
||||
// 判断传入的id是否都存在于数据库中
|
||||
const isContain = permissionIds.every((item) =>
|
||||
allPermissionIds.includes(item),
|
||||
);
|
||||
|
||||
if (!isContain)
|
||||
throw new HttpException(
|
||||
'出现不存在的权限id,请检查传入的permissionIds!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
|
||||
const data = permissionIds.map((permissionId) => ({
|
||||
roleId: id,
|
||||
permissionId,
|
||||
}));
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.sysRolePermission.deleteMany({
|
||||
where: { permissionId: { in: permissionIds } },
|
||||
}),
|
||||
this.prisma.sysRolePermission.createMany({ data }),
|
||||
]);
|
||||
}
|
||||
}
|
||||
7
server/src/system/auth/dto/auth.dto.ts
Normal file
7
server/src/system/auth/dto/auth.dto.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { CreateUserDto } from 'src/system/user/dto/create-user.dto';
|
||||
import { PickType } from '@nestjs/mapped-types';
|
||||
|
||||
export class AuthDto extends PickType(CreateUserDto, [
|
||||
'account',
|
||||
'password',
|
||||
] as const) {}
|
||||
8
server/src/system/auth/dto/role-permission.dto.ts
Normal file
8
server/src/system/auth/dto/role-permission.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ArrayUnique, IsInt, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class RolePermissionDto {
|
||||
@IsNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@ArrayUnique()
|
||||
permissionIds: number[];
|
||||
}
|
||||
8
server/src/system/auth/dto/user-role.dto.ts
Normal file
8
server/src/system/auth/dto/user-role.dto.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ArrayUnique, IsInt, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class UserRoleDto {
|
||||
@IsNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@ArrayUnique()
|
||||
roleIds: number[];
|
||||
}
|
||||
22
server/src/system/auth/roles.guard.ts
Normal file
22
server/src/system/auth/roles.guard.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Role } from '@/common/enum';
|
||||
import { ROLES_KEY } from '@/common/decorators/roles.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles) {
|
||||
return true;
|
||||
}
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
return requiredRoles.some((role) => user.roles?.includes(role));
|
||||
}
|
||||
}
|
||||
49
server/src/system/config/config.controller.ts
Normal file
49
server/src/system/config/config.controller.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from './config.service';
|
||||
import { CreateConfigDto } from './dto/create-config.dto';
|
||||
import { UpdateConfigDto } from './dto/update-config.dto';
|
||||
import { QueryListDto } from './dto/query-list.dto';
|
||||
|
||||
@Controller()
|
||||
export class ConfigController {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateConfigDto) {
|
||||
return this.configService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll(@Query() dto: QueryListDto) {
|
||||
return this.configService.findAll(dto);
|
||||
}
|
||||
|
||||
@Get(':key')
|
||||
findOne(@Param('key') key: string) {
|
||||
return this.configService.findOne(key);
|
||||
}
|
||||
|
||||
@Patch(':key')
|
||||
update(@Param('key') key: string, @Body() updateConfigDto: UpdateConfigDto) {
|
||||
return this.configService.update(key, updateConfigDto);
|
||||
}
|
||||
|
||||
@Delete(':key')
|
||||
remove(@Param('key') key: string) {
|
||||
return this.configService.remove(key);
|
||||
}
|
||||
|
||||
@Post('sync')
|
||||
syncConfig() {
|
||||
return this.configService.syncConfig();
|
||||
}
|
||||
}
|
||||
9
server/src/system/config/config.module.ts
Normal file
9
server/src/system/config/config.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigService } from './config.service';
|
||||
import { ConfigController } from './config.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [ConfigController],
|
||||
providers: [ConfigService],
|
||||
})
|
||||
export class ConfigModule {}
|
||||
99
server/src/system/config/config.service.ts
Normal file
99
server/src/system/config/config.service.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Injectable, HttpException, HttpStatus, Inject } from '@nestjs/common';
|
||||
import { CreateConfigDto } from './dto/create-config.dto';
|
||||
import { UpdateConfigDto } from './dto/update-config.dto';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
import { QueryListDto } from './dto/query-list.dto';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { RedisGroup } from '@/common/enum';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cacheService: Cache,
|
||||
) {}
|
||||
|
||||
findOneByUnique(key: string) {
|
||||
return this.prisma.sysConfig.findUnique({
|
||||
where: {
|
||||
key,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: CreateConfigDto) {
|
||||
const doesExists = await this.findOneByUnique(data.key);
|
||||
|
||||
if (doesExists) {
|
||||
throw new HttpException('key值不可重复!', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
await this.prisma.sysConfig.create({ data });
|
||||
}
|
||||
|
||||
async findAll({ page, pageSize }: QueryListDto): Promise<QueryListResult> {
|
||||
const query: Prisma.SysConfigFindManyArgs = {
|
||||
select: {
|
||||
key: true,
|
||||
value: true,
|
||||
remark: true,
|
||||
},
|
||||
skip: pageSize * (page - 1),
|
||||
take: pageSize,
|
||||
};
|
||||
|
||||
const [list, count] = await this.prisma.$transaction([
|
||||
this.prisma.sysConfig.findMany(query),
|
||||
this.prisma.sysConfig.count({ where: query.where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
count,
|
||||
list,
|
||||
};
|
||||
}
|
||||
|
||||
findOne(key: string) {
|
||||
return this.findOneByUnique(key);
|
||||
}
|
||||
|
||||
async update(key: string, updateConfigDto: UpdateConfigDto) {
|
||||
const doesExists = await this.findOneByUnique(key);
|
||||
|
||||
if (!doesExists) {
|
||||
throw new HttpException('修改失败!key值不存在!', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
await this.prisma.sysConfig.update({
|
||||
data: updateConfigDto,
|
||||
where: { key },
|
||||
});
|
||||
}
|
||||
|
||||
async remove(key: string) {
|
||||
const doesExists = await this.findOneByUnique(key);
|
||||
|
||||
if (!doesExists) {
|
||||
throw new HttpException('删除失败!key值不存在!', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
await this.prisma.sysConfig.delete({ where: { key } });
|
||||
}
|
||||
|
||||
async syncConfig() {
|
||||
const allConfigs = await this.prisma.sysConfig.findMany();
|
||||
|
||||
const keys = await this.cacheService.store.keys(`${RedisGroup.Config}:*`);
|
||||
if (keys.length) await this.cacheService.store.mdel(...keys);
|
||||
|
||||
const config: [string, unknown][] = allConfigs.map((item) => [
|
||||
item.key,
|
||||
item.value,
|
||||
]);
|
||||
|
||||
if (config.length) await this.cacheService.store.mset(config, 0);
|
||||
}
|
||||
}
|
||||
14
server/src/system/config/dto/create-config.dto.ts
Normal file
14
server/src/system/config/dto/create-config.dto.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateConfigDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
key: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
value: string;
|
||||
|
||||
@IsOptional()
|
||||
remark: string;
|
||||
}
|
||||
3
server/src/system/config/dto/query-list.dto.ts
Normal file
3
server/src/system/config/dto/query-list.dto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PaginationDto } from 'src/common/dto/pagination.dto';
|
||||
|
||||
export class QueryListDto extends PaginationDto {}
|
||||
6
server/src/system/config/dto/update-config.dto.ts
Normal file
6
server/src/system/config/dto/update-config.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateConfigDto } from './create-config.dto';
|
||||
|
||||
export class UpdateConfigDto extends PartialType(
|
||||
OmitType(CreateConfigDto, ['key'] as const),
|
||||
) {}
|
||||
23
server/src/system/files/files.controller.ts
Normal file
23
server/src/system/files/files.controller.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
ParseFilePipe,
|
||||
} from '@nestjs/common';
|
||||
import { FilesService } from './files.service';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
|
||||
@Controller()
|
||||
export class FilesController {
|
||||
constructor(private readonly filesService: FilesService) {}
|
||||
|
||||
@Post()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
upload(
|
||||
@UploadedFile(new ParseFilePipe({ validators: [] }))
|
||||
file: Express.Multer.File,
|
||||
) {
|
||||
return this.filesService.upload(file);
|
||||
}
|
||||
}
|
||||
10
server/src/system/files/files.module.ts
Normal file
10
server/src/system/files/files.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FilesService } from './files.service';
|
||||
import { FilesController } from './files.controller';
|
||||
import { MinioService } from '@/minio/minio.service';
|
||||
|
||||
@Module({
|
||||
controllers: [FilesController],
|
||||
providers: [FilesService, MinioService],
|
||||
})
|
||||
export class FilesModule {}
|
||||
31
server/src/system/files/files.service.ts
Normal file
31
server/src/system/files/files.service.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
|
||||
import path from 'path';
|
||||
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import { MinioService } from '@/minio/minio.service';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
@Injectable()
|
||||
export class FilesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cacheService: Cache,
|
||||
private readonly minioService: MinioService,
|
||||
) {}
|
||||
|
||||
async upload(file: Express.Multer.File) {
|
||||
const fileName = `${nanoid()}${path.extname(file.originalname)}`;
|
||||
|
||||
const filePath = await this.minioService.uploadFile(file, fileName);
|
||||
|
||||
const res = await this.prisma.files.create({
|
||||
data: { name: fileName, path: filePath },
|
||||
});
|
||||
|
||||
return {
|
||||
id: res.id,
|
||||
path: res.path,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
server/src/system/menu/dto/create-menu.dto.ts
Normal file
15
server/src/system/menu/dto/create-menu.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateMenuDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
url: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
parentId?: number;
|
||||
}
|
||||
4
server/src/system/menu/dto/update-menu.dto.ts
Normal file
4
server/src/system/menu/dto/update-menu.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateMenuDto } from './create-menu.dto';
|
||||
|
||||
export class UpdateMenuDto extends PartialType(CreateMenuDto) {}
|
||||
42
server/src/system/menu/menu.controller.ts
Normal file
42
server/src/system/menu/menu.controller.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { MenuService } from './menu.service';
|
||||
import { CreateMenuDto } from './dto/create-menu.dto';
|
||||
import { UpdateMenuDto } from './dto/update-menu.dto';
|
||||
|
||||
@Controller()
|
||||
export class MenuController {
|
||||
constructor(private readonly menuService: MenuService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateMenuDto) {
|
||||
return this.menuService.create(dto);
|
||||
}
|
||||
|
||||
@Get('tree')
|
||||
getTreeMenu() {
|
||||
return this.menuService.getTreeMenu();
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.menuService.findAll();
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() updateMenuDto: UpdateMenuDto) {
|
||||
return this.menuService.update(+id, updateMenuDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: number) {
|
||||
return this.menuService.remove(id);
|
||||
}
|
||||
}
|
||||
9
server/src/system/menu/menu.module.ts
Normal file
9
server/src/system/menu/menu.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MenuService } from './menu.service';
|
||||
import { MenuController } from './menu.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [MenuController],
|
||||
providers: [MenuService],
|
||||
})
|
||||
export class MenuModule {}
|
||||
126
server/src/system/menu/menu.service.ts
Normal file
126
server/src/system/menu/menu.service.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { CreateMenuDto } from './dto/create-menu.dto';
|
||||
import { UpdateMenuDto } from './dto/update-menu.dto';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PermissionType } from '@/common/enum';
|
||||
|
||||
@Injectable()
|
||||
export class MenuService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
findOne(params: { id?: number; excludeId?: number }) {
|
||||
const { id, excludeId } = params;
|
||||
return this.prisma.sysMenu.findFirst({
|
||||
where: {
|
||||
...(id ? { id } : {}),
|
||||
...(excludeId ? { NOT: { id: excludeId } } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
findOneById(id: number) {
|
||||
return this.prisma.sysMenu.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
async create(data: CreateMenuDto) {
|
||||
await this.prisma.$transaction(async (prisma) => {
|
||||
const { id: menuId } = await prisma.sysMenu.create({ data });
|
||||
// 创建菜单权限
|
||||
const { id: permissionId } = await prisma.sysPermission.create({
|
||||
data: { type: PermissionType.Menu },
|
||||
});
|
||||
// 关联权限和菜单
|
||||
await prisma.sysMenuPermission.create({ data: { menuId, permissionId } });
|
||||
});
|
||||
}
|
||||
|
||||
generatorMenu(data, parentId = 0) {
|
||||
const menu = [];
|
||||
for (const item of data) {
|
||||
if (item.parentId === parentId) {
|
||||
const children = this.generatorMenu(data, item.id);
|
||||
if (children.length) {
|
||||
item.children = children;
|
||||
}
|
||||
menu.push(item);
|
||||
}
|
||||
}
|
||||
return menu;
|
||||
}
|
||||
|
||||
async getTreeMenu() {
|
||||
const query: Prisma.SysMenuFindManyArgs = {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
parentId: true,
|
||||
},
|
||||
};
|
||||
const list = await this.prisma.sysMenu.findMany(query);
|
||||
return this.generatorMenu(list);
|
||||
}
|
||||
|
||||
findAll() {
|
||||
const query: Prisma.SysMenuFindManyArgs = {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
url: true,
|
||||
parentId: true,
|
||||
},
|
||||
};
|
||||
return this.prisma.sysMenu.findMany(query);
|
||||
}
|
||||
|
||||
async update(id: number, data: UpdateMenuDto) {
|
||||
const doesExists = await this.findOne({ id });
|
||||
if (!doesExists) {
|
||||
throw new HttpException('菜单不存在!', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (data.parentId === id) {
|
||||
throw new HttpException(
|
||||
'父级id不可与自身id相同!',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
await this.prisma.sysMenu.update({ data, where: { id } });
|
||||
}
|
||||
|
||||
async remove(menuId: number) {
|
||||
const doesExists = await this.findOneById(menuId);
|
||||
if (!doesExists) {
|
||||
throw new HttpException('菜单不存在!', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// 判断是否存在子菜单
|
||||
const doesExistsChildren = await this.prisma.sysMenu.findFirst({
|
||||
where: { parentId: menuId },
|
||||
});
|
||||
|
||||
if (doesExistsChildren) {
|
||||
throw new HttpException(
|
||||
'删除失败:请确保当前菜单下不存在子菜单!',
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (prisma) => {
|
||||
// step1 通过关联表删除权限表数据 这一步必须在前
|
||||
// 因为schema文件中设置了onDelete: Cascade 删除菜单会删除关联表数据 则无法找出对应权限
|
||||
await prisma.sysPermission.deleteMany({
|
||||
where: {
|
||||
sysMenuPermission: {
|
||||
some: { menuId },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
//step2 删除菜单数据
|
||||
await prisma.sysMenu.delete({ where: { id: menuId } });
|
||||
});
|
||||
}
|
||||
}
|
||||
6
server/src/system/permission/dto/grant-menu.dto.ts
Normal file
6
server/src/system/permission/dto/grant-menu.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { IsNumber } from 'class-validator';
|
||||
|
||||
export class GrantMenuDto {
|
||||
@IsNumber({}, { each: true })
|
||||
menuIds: number[];
|
||||
}
|
||||
19
server/src/system/permission/permission.controller.ts
Normal file
19
server/src/system/permission/permission.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Controller, Body, Patch, Param, Get } from '@nestjs/common';
|
||||
import { PermissionService } from './permission.service';
|
||||
|
||||
import { GrantMenuDto } from '@/system/permission/dto/grant-menu.dto';
|
||||
|
||||
@Controller()
|
||||
export class PermissionController {
|
||||
constructor(private readonly permissionService: PermissionService) {}
|
||||
|
||||
@Get(':id/menu')
|
||||
getMenu(@Param('id') id: number) {
|
||||
return this.permissionService.getMenu(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
grantMenu(@Param('id') id: number, @Body() dto: GrantMenuDto) {
|
||||
return this.permissionService.grantMenu(id, dto);
|
||||
}
|
||||
}
|
||||
9
server/src/system/permission/permission.module.ts
Normal file
9
server/src/system/permission/permission.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { PermissionController } from './permission.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [PermissionController],
|
||||
providers: [PermissionService],
|
||||
})
|
||||
export class PermissionModule {}
|
||||
45
server/src/system/permission/permission.service.ts
Normal file
45
server/src/system/permission/permission.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { GrantMenuDto } from '@/system/permission/dto/grant-menu.dto';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async getMenu(id: number) {
|
||||
const menuIds = await this.prisma.sysMenuPermission.findMany({
|
||||
select: {
|
||||
menuId: true,
|
||||
},
|
||||
where: {
|
||||
permission: {
|
||||
sysRolePermission: {
|
||||
some: { roleId: id },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return menuIds.map((item) => item.menuId);
|
||||
}
|
||||
|
||||
async grantMenu(id: number, { menuIds }: GrantMenuDto) {
|
||||
await this.prisma.$transaction(async (prisma) => {
|
||||
// 删除角色对应的权限
|
||||
await prisma.sysRolePermission.deleteMany({
|
||||
where: { roleId: id },
|
||||
});
|
||||
// 通过菜单id 找出所有权限id
|
||||
const rolePermIds = await prisma.sysMenuPermission.findMany({
|
||||
select: { permissionId: true },
|
||||
where: { menuId: { in: menuIds } },
|
||||
});
|
||||
// 根据权限id 关联菜单和权限
|
||||
if (rolePermIds.length > 0) {
|
||||
await prisma.sysRolePermission.createMany({
|
||||
data: rolePermIds.map((item) => ({ ...item, roleId: id })),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
12
server/src/system/role/dto/create-role.dto.ts
Normal file
12
server/src/system/role/dto/create-role.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IsNotEmpty, IsNumber, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateRoleDto {
|
||||
@IsOptional()
|
||||
name: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
code: string;
|
||||
|
||||
@IsNumber()
|
||||
status: number;
|
||||
}
|
||||
3
server/src/system/role/dto/query-list.dto.ts
Normal file
3
server/src/system/role/dto/query-list.dto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PaginationDto } from 'src/common/dto/pagination.dto';
|
||||
|
||||
export class QueryListDto extends PaginationDto {}
|
||||
6
server/src/system/role/dto/update-role.dto.ts
Normal file
6
server/src/system/role/dto/update-role.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateRoleDto } from './create-role.dto';
|
||||
|
||||
export class UpdateRoleDto extends PartialType(
|
||||
OmitType(CreateRoleDto, ['code']),
|
||||
) {}
|
||||
44
server/src/system/role/role.controller.ts
Normal file
44
server/src/system/role/role.controller.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { RoleService } from './role.service';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||
import { QueryListDto } from './dto/query-list.dto';
|
||||
|
||||
@Controller()
|
||||
export class RoleController {
|
||||
constructor(private readonly roleService: RoleService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateRoleDto) {
|
||||
return this.roleService.create(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findPage(@Query() dto: QueryListDto): Promise<QueryListResult> {
|
||||
return this.roleService.findPage(dto);
|
||||
}
|
||||
|
||||
@Get('/all')
|
||||
findAll() {
|
||||
return this.roleService.findAll();
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
|
||||
return this.roleService.update(+id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.roleService.remove(+id);
|
||||
}
|
||||
}
|
||||
9
server/src/system/role/role.module.ts
Normal file
9
server/src/system/role/role.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RoleService } from './role.service';
|
||||
import { RoleController } from './role.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [RoleController],
|
||||
providers: [RoleService],
|
||||
})
|
||||
export class RoleModule {}
|
||||
81
server/src/system/role/role.service.ts
Normal file
81
server/src/system/role/role.service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { CreateRoleDto } from './dto/create-role.dto';
|
||||
import { UpdateRoleDto } from './dto/update-role.dto';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
import { Prisma, SysRole } from '@prisma/client';
|
||||
import { QueryListDto } from './dto/query-list.dto';
|
||||
|
||||
@Injectable()
|
||||
export class RoleService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findOne(
|
||||
params: Prisma.SysRoleWhereUniqueInput,
|
||||
): Promise<SysRole | null> {
|
||||
return this.prisma.sysRole.findUnique({
|
||||
where: params,
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: CreateRoleDto) {
|
||||
const doesExists = await this.findOne({ code: data.code });
|
||||
if (doesExists)
|
||||
throw new HttpException('角色编码不可重复!', HttpStatus.NOT_FOUND);
|
||||
|
||||
await this.prisma.sysRole.create({ data });
|
||||
}
|
||||
|
||||
async findPage({ page, pageSize }: QueryListDto): Promise<QueryListResult> {
|
||||
const query: Prisma.SysRoleFindManyArgs = {
|
||||
skip: pageSize * (page - 1),
|
||||
take: pageSize,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
status: true,
|
||||
},
|
||||
};
|
||||
const [list, count] = await this.prisma.$transaction([
|
||||
this.prisma.sysRole.findMany(query),
|
||||
this.prisma.sysRole.count({ where: query.where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
count,
|
||||
list,
|
||||
};
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return this.prisma.sysRole.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
code: true,
|
||||
status: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async update(id: number, data: UpdateRoleDto) {
|
||||
const role = await this.findOne({ id });
|
||||
if (!role)
|
||||
throw new HttpException('角色信息不存在!', HttpStatus.NOT_FOUND);
|
||||
|
||||
await this.prisma.sysRole.update({
|
||||
data,
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const role = await this.findOne({ id });
|
||||
if (!role)
|
||||
throw new HttpException('角色信息不存在!', HttpStatus.NOT_FOUND);
|
||||
|
||||
await this.prisma.sysRole.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
58
server/src/system/system.module.ts
Normal file
58
server/src/system/system.module.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RouterModule } from '@nestjs/core';
|
||||
import { UserModule } from './user/user.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { RoleModule } from './role/role.module';
|
||||
import { FilesModule } from './files/files.module';
|
||||
import { MenuModule } from './menu/menu.module';
|
||||
import { ConfigModule } from './config/config.module';
|
||||
import { PermissionModule } from './permission/permission.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
RouterModule.register([
|
||||
{
|
||||
path: 'admin',
|
||||
module: SystemModule,
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
module: UserModule,
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
module: AuthModule,
|
||||
},
|
||||
{
|
||||
path: 'role',
|
||||
module: RoleModule,
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
module: FilesModule,
|
||||
},
|
||||
{
|
||||
path: 'menu',
|
||||
module: MenuModule,
|
||||
},
|
||||
{
|
||||
path: 'config',
|
||||
module: ConfigModule,
|
||||
},
|
||||
{
|
||||
path: 'permission',
|
||||
module: PermissionModule,
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
UserModule,
|
||||
AuthModule,
|
||||
RoleModule,
|
||||
FilesModule,
|
||||
MenuModule,
|
||||
ConfigModule,
|
||||
PermissionModule,
|
||||
],
|
||||
})
|
||||
export class SystemModule {}
|
||||
35
server/src/system/user/dto/create-user.dto.ts
Normal file
35
server/src/system/user/dto/create-user.dto.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateUserDto {
|
||||
@IsOptional()
|
||||
@MaxLength(20, { message: '用户名不要超过20个字符' })
|
||||
name?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@MinLength(5, { message: '账户名不要低于6位字符' })
|
||||
@MaxLength(50, { message: '账户名不要超过50个字符' })
|
||||
account: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
avatarId: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
gender: number;
|
||||
|
||||
@IsNotEmpty()
|
||||
@MinLength(6, { message: '密码不要低于6位字符' })
|
||||
@MaxLength(20, { message: '密码不要超过50个字符' })
|
||||
password: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
status?: number;
|
||||
}
|
||||
3
server/src/system/user/dto/query-list.dto.ts
Normal file
3
server/src/system/user/dto/query-list.dto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { PaginationDto } from 'src/common/dto/pagination.dto';
|
||||
|
||||
export class QueryListDto extends PaginationDto {}
|
||||
10
server/src/system/user/dto/update-password.dto.ts
Normal file
10
server/src/system/user/dto/update-password.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { IsOptional, MaxLength, MinLength } from 'class-validator';
|
||||
import { PickType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
export class UpdatePasswordDto extends PickType(CreateUserDto, ['password']) {
|
||||
@IsOptional()
|
||||
@MinLength(5)
|
||||
@MaxLength(50)
|
||||
newPassword?: string;
|
||||
}
|
||||
6
server/src/system/user/dto/update-user.dto.ts
Normal file
6
server/src/system/user/dto/update-user.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { OmitType, PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateUserDto } from './create-user.dto';
|
||||
|
||||
export class UpdateUserDto extends PartialType(
|
||||
OmitType(CreateUserDto, ['account', 'password'] as const),
|
||||
) {}
|
||||
58
server/src/system/user/user.controller.ts
Normal file
58
server/src/system/user/user.controller.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { QueryListDto } from './dto/query-list.dto';
|
||||
import { UpdatePasswordDto } from './dto/update-password.dto';
|
||||
|
||||
@Controller()
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Post()
|
||||
create(@Body() data: CreateUserDto) {
|
||||
return this.userService.create(data);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll(@Query() dto: QueryListDto): Promise<QueryListResult> {
|
||||
return this.userService.findAll(dto);
|
||||
}
|
||||
|
||||
@Get('/info')
|
||||
getUserInfo(@Request() req) {
|
||||
const { sub } = req['user'];
|
||||
const { isAdmin } = req;
|
||||
return this.userService.getUserInfo(sub, isAdmin);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOneById(@Param('id') id: number) {
|
||||
return this.userService.findOneById(id);
|
||||
}
|
||||
|
||||
@Patch(':id/password')
|
||||
updatePassword(@Param('id') id: string, @Body() dto: UpdatePasswordDto) {
|
||||
return this.userService.updatePassword(+id, dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
||||
return this.userService.update(+id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string) {
|
||||
return this.userService.remove(+id);
|
||||
}
|
||||
}
|
||||
9
server/src/system/user/user.module.ts
Normal file
9
server/src/system/user/user.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
})
|
||||
export class UserModule {}
|
||||
208
server/src/system/user/user.service.ts
Normal file
208
server/src/system/user/user.service.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import {
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { Prisma, SysUser } from '@prisma/client';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
import { QueryListDto } from './dto/query-list.dto';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { UpdatePasswordDto } from './dto/update-password.dto';
|
||||
import { SkipAuth } from '@/common/decorators/skip-auth.decorator';
|
||||
import { RedisGroup } from '@/common/enum';
|
||||
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
|
||||
@Injectable()
|
||||
@SkipAuth()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
) {}
|
||||
|
||||
async findOne(
|
||||
params: Prisma.SysUserWhereUniqueInput,
|
||||
): Promise<SysUser | null> {
|
||||
return this.prisma.sysUser.findUnique({
|
||||
where: params,
|
||||
});
|
||||
}
|
||||
|
||||
async create(dto: CreateUserDto) {
|
||||
const userExists = await this.findOne({ account: dto.account });
|
||||
if (userExists)
|
||||
throw new HttpException('账号已存在!', HttpStatus.UNPROCESSABLE_ENTITY);
|
||||
|
||||
const passwordSalt = await bcrypt.genSalt();
|
||||
const password = await bcrypt.hash(dto.password, passwordSalt);
|
||||
const data = {
|
||||
...dto,
|
||||
passwordSalt,
|
||||
password,
|
||||
};
|
||||
|
||||
await this.prisma.sysUser.create({ data });
|
||||
}
|
||||
|
||||
async findAll({ page, pageSize }: QueryListDto): Promise<QueryListResult> {
|
||||
const query: Prisma.SysUserFindManyArgs = {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
account: true,
|
||||
status: true,
|
||||
gender: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
avatar: {
|
||||
select: {
|
||||
id: true,
|
||||
path: true,
|
||||
},
|
||||
},
|
||||
userRole: {
|
||||
select: { roleId: true },
|
||||
},
|
||||
},
|
||||
skip: pageSize * (page - 1),
|
||||
take: pageSize,
|
||||
};
|
||||
|
||||
const [list, count] = await this.prisma.$transaction([
|
||||
this.prisma.sysUser.findMany(query),
|
||||
this.prisma.sysUser.count({ where: query.where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
page,
|
||||
pageSize,
|
||||
count,
|
||||
list,
|
||||
};
|
||||
}
|
||||
|
||||
async findOneById(id: number) {
|
||||
return this.prisma.sysUser.findUnique({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
account: true,
|
||||
status: true,
|
||||
gender: true,
|
||||
avatar: {
|
||||
select: {
|
||||
id: true,
|
||||
path: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getUserInfo(id: number, isAdmin: boolean) {
|
||||
const user = await this.prisma.sysUser.findUnique({
|
||||
select: {
|
||||
name: true,
|
||||
gender: true,
|
||||
account: true,
|
||||
avatar: {
|
||||
select: {
|
||||
path: true,
|
||||
},
|
||||
},
|
||||
createdAt: true,
|
||||
},
|
||||
where: {
|
||||
id,
|
||||
status: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const query: Prisma.SysMenuFindManyArgs = { where: {} };
|
||||
|
||||
if (!isAdmin) {
|
||||
query.where = {
|
||||
sysMenuPermission: {
|
||||
some: {
|
||||
permission: {
|
||||
sysRolePermission: {
|
||||
some: { role: { sysUserRole: { some: { userId: id } } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const menus = await this.prisma.sysMenu.findMany(query);
|
||||
return {
|
||||
...user,
|
||||
menus,
|
||||
};
|
||||
}
|
||||
|
||||
async update(id: number, data: UpdateUserDto) {
|
||||
const user = await this.findOne({ id });
|
||||
if (!user)
|
||||
throw new HttpException('修改失败,用户不存在!', HttpStatus.NOT_FOUND);
|
||||
|
||||
await this.prisma.$transaction(async (prisma) => {
|
||||
await prisma.sysUser.update({
|
||||
data,
|
||||
where: { id },
|
||||
});
|
||||
|
||||
// 禁用用户 删除token
|
||||
if (data.status === 0) {
|
||||
if (data.status === 0) {
|
||||
const keys = await this.cacheManager.store.keys(
|
||||
`${RedisGroup.JwtJti}:${id}:*`,
|
||||
);
|
||||
await this.cacheManager.store.mdel(...keys);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async updatePassword(
|
||||
id: number,
|
||||
{ password, newPassword }: UpdatePasswordDto,
|
||||
) {
|
||||
const user = await this.findOne({ id });
|
||||
if (!user)
|
||||
throw new HttpException('修改失败,用户不存在!', HttpStatus.NOT_FOUND);
|
||||
// 同时存在密码和新密码
|
||||
const data: Partial<SysUser> = {};
|
||||
|
||||
const comparePasswordSalt = user.passwordSalt;
|
||||
const comparePassword = await bcrypt.hash(password, comparePasswordSalt);
|
||||
|
||||
if (comparePassword !== user.password) {
|
||||
throw new UnauthorizedException('密码错误,修改失败!');
|
||||
} else {
|
||||
data.passwordSalt = await bcrypt.genSalt();
|
||||
data.password = await bcrypt.hash(newPassword, data.passwordSalt);
|
||||
}
|
||||
|
||||
await this.prisma.sysUser.update({
|
||||
data,
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async remove(id: number) {
|
||||
const user = await this.findOne({ id });
|
||||
|
||||
if (!user)
|
||||
throw new HttpException('删除失败,用户不存在!', HttpStatus.NOT_FOUND);
|
||||
|
||||
await this.prisma.sysUser.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
0
server/src/utils/index.ts
Normal file
0
server/src/utils/index.ts
Normal file
4
server/tsconfig.build.json
Normal file
4
server/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
30
server/tsconfig.json
Normal file
30
server/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "esnext",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"prisma/**/*",
|
||||
"src/**/*",
|
||||
"type/**/*"
|
||||
]
|
||||
}
|
||||
64
server/type/global.ts
Normal file
64
server/type/global.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { RedisGroup } from '@/common/enum';
|
||||
|
||||
declare global {
|
||||
interface QueryListResult {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
count: number;
|
||||
list: any[];
|
||||
}
|
||||
|
||||
interface IRedisGetOptions {
|
||||
group?: RedisGroup;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface IRedisSetOptions {
|
||||
group?: RedisGroup;
|
||||
key: string;
|
||||
value: unknown;
|
||||
ttl?: number;
|
||||
}
|
||||
|
||||
interface IRedisDelOptions {
|
||||
group?: RedisGroup;
|
||||
key: string;
|
||||
}
|
||||
|
||||
interface IRedisMgetOptions {
|
||||
group?: RedisGroup;
|
||||
keys: string[];
|
||||
}
|
||||
|
||||
interface IRedisKeysOptions {
|
||||
group?: RedisGroup;
|
||||
pattern: string;
|
||||
}
|
||||
|
||||
interface IRedisMsetOptions {
|
||||
group?: RedisGroup;
|
||||
data: { key: string; value: string }[];
|
||||
ttl?: number;
|
||||
}
|
||||
|
||||
interface IRedisMdelOptions {
|
||||
group?: RedisGroup;
|
||||
keys: string[];
|
||||
}
|
||||
|
||||
interface IRedisService {
|
||||
get<T>(args: IRedisGetOptions): Promise<T>;
|
||||
|
||||
set(args: IRedisSetOptions): Promise<void>;
|
||||
|
||||
del(args: IRedisDelOptions): Promise<void>;
|
||||
|
||||
mget<T>(args: IRedisMgetOptions): Promise<T>;
|
||||
|
||||
keys(args: IRedisKeysOptions): Promise<string[]>;
|
||||
|
||||
mset(args: IRedisMsetOptions): Promise<void>;
|
||||
|
||||
mdel(args: IRedisMdelOptions): Promise<void>;
|
||||
}
|
||||
}
|
||||
5674
server/yarn.lock
Normal file
5674
server/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user