09032019
This commit is contained in:
parent
835a1ca534
commit
312cfbb658
|
@ -24,12 +24,18 @@
|
|||
"@nestjs/common": "^10.3.10",
|
||||
"@nestjs/config": "^3.2.3",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/schedule": "^4.1.0",
|
||||
"@nestjs/websockets": "^10.3.10",
|
||||
"@nicestack/common": "workspace:^",
|
||||
"@trpc/server": "11.0.0-rc.456",
|
||||
"axios": "^1.7.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^5.12.0",
|
||||
"cron": "^3.1.7",
|
||||
"dayjs": "^1.11.13",
|
||||
"exceljs": "^4.4.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
|
@ -37,21 +43,20 @@
|
|||
"socket.io": "^4.7.5",
|
||||
"superjson-cjs": "^2.2.3",
|
||||
"tus-js-client": "^4.1.0",
|
||||
"zod": "^3.23.8",
|
||||
"dayjs": "^1.11.13",
|
||||
"exceljs": "^4.4.0"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/exceljs": "^1.3.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
}
|
|
@ -1,6 +1,4 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { TrpcModule } from './trpc/trpc.module';
|
||||
import { RedisService } from './redis/redis.service';
|
||||
|
||||
|
@ -8,11 +6,11 @@ import { RedisModule } from './redis/redis.module';
|
|||
import { SocketGateway } from './socket/socket.gateway';
|
||||
import { QueueModule } from './queue/queue.module';
|
||||
import { TransformModule } from './transform/transform.module';
|
||||
import { ControllerService } from './controller/controller.service';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [TrpcModule, RedisModule, QueueModule, TransformModule],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, RedisService, SocketGateway, ControllerService],
|
||||
imports: [ScheduleModule.forRoot(), TrpcModule, RedisModule, QueueModule, TransformModule, AuthModule],
|
||||
providers: [RedisService, SocketGateway],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { env } from '@server/env';
|
||||
import { Request } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private jwtService: JwtService) { }
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(
|
||||
token,
|
||||
{
|
||||
secret: env.JWT_SECRET
|
||||
}
|
||||
);
|
||||
// 💡 We're assigning the payload to the request object here
|
||||
// so that we can access it in our route handlers
|
||||
request['user'] = payload;
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { env } from '@server/env';
|
||||
|
||||
@Module({
|
||||
providers: [AuthService],
|
||||
imports: [JwtModule.register({
|
||||
global: true,
|
||||
secret: env.JWT_SECRET,
|
||||
signOptions: { expiresIn: '60s' },
|
||||
}),]
|
||||
})
|
||||
export class AuthModule { }
|
|
@ -0,0 +1,27 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthSchema } from '@nicestack/common';
|
||||
|
||||
@Injectable()
|
||||
export class AuthRouter {
|
||||
constructor(private readonly trpc: TrpcService, private readonly authService: AuthService) { }
|
||||
|
||||
router = this.trpc.router({
|
||||
login: this.trpc.procedure.input(AuthSchema.signInRequset).mutation(({ input }) => {
|
||||
return this.authService.signIn(input);
|
||||
}),
|
||||
signup: this.trpc.procedure.input(AuthSchema.signUpRequest).mutation(({ input }) => {
|
||||
return this.authService.signUp(input);
|
||||
}),
|
||||
refreshToken: this.trpc.procedure.input(AuthSchema.refreshTokenRequest).mutation(({ input }) => {
|
||||
return this.authService.refreshToken(input);
|
||||
}),
|
||||
logout: this.trpc.protectProcedure.input(AuthSchema.logoutRequest).mutation(({ input }) => {
|
||||
return this.authService.logout(input);
|
||||
}),
|
||||
changePassword: this.trpc.protectProcedure.input(AuthSchema.changePassword).mutation(({ input }) => {
|
||||
return this.authService.changePassword(input);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { AuthSchema, db, z } from '@nicestack/common';
|
||||
import { StaffService } from '@server/models/staff/staff.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly staffService: StaffService,
|
||||
) { }
|
||||
async signIn(data: z.infer<typeof AuthSchema.signInRequset>) {
|
||||
const { username, password } = data;
|
||||
const staff = await db.staff.findUnique({ where: { username } });
|
||||
if (!staff) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
const isPasswordMatch = await bcrypt.compare(password, staff.password);
|
||||
|
||||
if (!isPasswordMatch) {
|
||||
throw new UnauthorizedException('Invalid username or password');
|
||||
}
|
||||
|
||||
const payload = { sub: staff.id, username: staff.username };
|
||||
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' });
|
||||
const refreshToken = await this.generateRefreshToken(staff.id);
|
||||
|
||||
// Store the refresh token in the database
|
||||
await db.refreshToken.create({
|
||||
data: {
|
||||
staffId: staff.id,
|
||||
token: refreshToken,
|
||||
},
|
||||
});
|
||||
return {
|
||||
access_token: accessToken,
|
||||
refresh_token: refreshToken,
|
||||
};
|
||||
}
|
||||
|
||||
async generateRefreshToken(userId: string): Promise<string> {
|
||||
const payload = { sub: userId };
|
||||
return this.jwtService.signAsync(payload, { expiresIn: '7d' }); // Set an appropriate expiration
|
||||
}
|
||||
|
||||
async refreshToken(data: z.infer<typeof AuthSchema.refreshTokenRequest>) {
|
||||
const { refreshToken } = data;
|
||||
let payload;
|
||||
try {
|
||||
payload = this.jwtService.verify(refreshToken);
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } });
|
||||
|
||||
if (!storedToken) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
const user = await db.staff.findUnique({ where: { id: payload.sub } });
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
const newAccessToken = await this.jwtService.signAsync(
|
||||
{ sub: user.id, username: user.username },
|
||||
{ expiresIn: '1h' },
|
||||
);
|
||||
|
||||
return {
|
||||
access_token: newAccessToken,
|
||||
};
|
||||
}
|
||||
|
||||
async signUp(data: z.infer<typeof AuthSchema.signUpRequest>) {
|
||||
const { username, password } = data;
|
||||
const existingUser = await db.staff.findUnique({ where: { username } });
|
||||
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('Username is already taken');
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
const staff = await this.staffService.create({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
return staff
|
||||
}
|
||||
|
||||
async logout(data: z.infer<typeof AuthSchema.logoutRequest>) {
|
||||
const { refreshToken } = data;
|
||||
await db.refreshToken.deleteMany({ where: { token: refreshToken } });
|
||||
return { message: 'Logout successful' };
|
||||
}
|
||||
|
||||
async changePassword(data: z.infer<typeof AuthSchema.changePassword>) {
|
||||
const { oldPassword, newPassword, username } = data;
|
||||
const user = await db.staff.findUnique({ where: { username } });
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
}
|
||||
const isPasswordMatch = await bcrypt.compare(oldPassword, user.password);
|
||||
if (!isPasswordMatch) {
|
||||
throw new UnauthorizedException('Old password is incorrect');
|
||||
}
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||
await this.staffService.update({ id: user.id, password: hashedNewPassword });
|
||||
|
||||
return { message: 'Password successfully changed' };
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const env: { JWT_SECRET: string } = {
|
||||
JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA='
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { HelloService } from './hello.service';
|
||||
|
||||
@Injectable()
|
||||
export class HelloRouter {
|
||||
constructor(private readonly trpc: TrpcService, private readonly hello: HelloService) { }
|
||||
router = this.trpc.router({
|
||||
hello: this.trpc.procedure.query(() => this.hello.helloWorld()),
|
||||
});
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class HelloService {
|
||||
helloWorld() {
|
||||
return {
|
||||
greeting: `Hello world`,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { InitService } from './init.service';
|
||||
|
||||
@Module({
|
||||
providers: [InitService],
|
||||
exports: [InitService]
|
||||
})
|
||||
export class InitModule { }
|
|
@ -0,0 +1,90 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { db, InitRoles, InitTaxonomies, ObjectType } from "@nicestack/common";
|
||||
import { AuthService } from '@server/auth/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class InitService {
|
||||
private readonly logger = new Logger(InitService.name);
|
||||
constructor(private readonly authService: AuthService) { }
|
||||
private async createRoles() {
|
||||
this.logger.log('Checking existing system roles');
|
||||
|
||||
for (const role of InitRoles) {
|
||||
const existingRole = await db.role.findUnique({
|
||||
where: { name: role.name },
|
||||
});
|
||||
|
||||
if (!existingRole) {
|
||||
this.logger.log(`Creating role: ${role.name}`);
|
||||
await db.role.create({
|
||||
data: { ...role, system: true },
|
||||
});
|
||||
} else {
|
||||
this.logger.log(`Role already exists: ${role.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
private async createTaxonomy() {
|
||||
this.logger.log('Checking existing taxonomies');
|
||||
|
||||
const existingTaxonomies = await db.taxonomy.findMany();
|
||||
const existingTaxonomyNames = existingTaxonomies.map(taxonomy => taxonomy.name);
|
||||
|
||||
for (const [index, taxonomy] of InitTaxonomies.entries()) {
|
||||
if (!existingTaxonomyNames.includes(taxonomy.name)) {
|
||||
this.logger.log(`Creating taxonomy: ${taxonomy.name}`);
|
||||
await db.taxonomy.create({
|
||||
data: {
|
||||
...taxonomy,
|
||||
order: index,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.logger.log(`Taxonomy already exists: ${taxonomy.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createRoot() {
|
||||
this.logger.log('Checking for root account');
|
||||
const rootAccountExists = await db.staff.findUnique({
|
||||
where: { phoneNumber: process.env.ADMIN_PHONE_NUMBER || '000000' },
|
||||
});
|
||||
if (!rootAccountExists) {
|
||||
this.logger.log('Creating root account');
|
||||
const rootStaff =await this.authService.signUp({
|
||||
username: 'root',
|
||||
password: 'admin'
|
||||
})
|
||||
const rootRole = await db.role.findUnique({
|
||||
where: { name: '根管理员' },
|
||||
});
|
||||
|
||||
if (rootRole) {
|
||||
this.logger.log('Assigning root role to root account');
|
||||
await db.roleMap.create({
|
||||
data: {
|
||||
objectType: ObjectType.STAFF,
|
||||
objectId: rootStaff.id,
|
||||
roleId: rootRole.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.logger.error('Root role does not exist');
|
||||
}
|
||||
} else {
|
||||
this.logger.log('Root account already exists');
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.logger.log('Initializing system roles');
|
||||
await this.createRoles();
|
||||
|
||||
this.logger.log('Initializing root account');
|
||||
await this.createRoot();
|
||||
|
||||
this.logger.log('Initializing taxonomies');
|
||||
await this.createTaxonomy();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { DepartmentService } from './department.service';
|
||||
import { DepartmentRouter } from './department.router';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
||||
@Module({
|
||||
providers: [DepartmentService, DepartmentRouter, TrpcService],
|
||||
exports: [DepartmentService, DepartmentRouter]
|
||||
})
|
||||
export class DepartmentModule { }
|
|
@ -0,0 +1,67 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { DepartmentService } from './department.service'; // assuming it's in the same directory
|
||||
import { DepartmentSchema, z } from '@nicestack/common';
|
||||
|
||||
@Injectable()
|
||||
export class DepartmentRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly departmentService: DepartmentService, // inject DepartmentService
|
||||
) {}
|
||||
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.protectProcedure
|
||||
.input(DepartmentSchema.create) // expect input according to the schema
|
||||
.mutation(async ({ input }) => {
|
||||
return this.departmentService.create(input);
|
||||
}),
|
||||
update: this.trpc.protectProcedure
|
||||
.input(DepartmentSchema.update) // expect input according to the schema
|
||||
.mutation(async ({ input }) => {
|
||||
return this.departmentService.update(input);
|
||||
}),
|
||||
|
||||
delete: this.trpc.protectProcedure
|
||||
.input(DepartmentSchema.delete) // expect input according to the schema
|
||||
.mutation(async ({ input }) => {
|
||||
return this.departmentService.delete(input);
|
||||
}),
|
||||
getDepartmentDetails: this.trpc.procedure
|
||||
.input(z.object({ deptId: z.string() })) // expect an object with deptId
|
||||
.query(async ({ input }) => {
|
||||
return this.departmentService.getDepartmentDetails(input.deptId);
|
||||
}),
|
||||
getAllChildDeptIds: this.trpc.procedure
|
||||
.input(z.object({ deptId: z.string() })) // expect an object with deptId
|
||||
.query(async ({ input }) => {
|
||||
return this.departmentService.getAllChildDeptIds(input.deptId);
|
||||
}),
|
||||
getAllParentDeptIds: this.trpc.procedure
|
||||
.input(z.object({ deptId: z.string() })) // expect an object with deptId
|
||||
.query(async ({ input }) => {
|
||||
return this.departmentService.getAllParentDeptIds(input.deptId);
|
||||
}),
|
||||
getChildren: this.trpc.procedure
|
||||
.input(z.object({ parentId: z.string().nullish() }))
|
||||
.query(async ({ input }) => {
|
||||
return this.departmentService.getChildren(input.parentId);
|
||||
}),
|
||||
getDomainDepartments: this.trpc.procedure
|
||||
.input(z.object({ query: z.string().nullish() }))
|
||||
.query(async ({ input }) => {
|
||||
const { query } = input;
|
||||
return this.departmentService.getDomainDepartments(query);
|
||||
}),
|
||||
findMany: this.trpc.procedure
|
||||
.input(DepartmentSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.departmentService.findMany(input);
|
||||
}),
|
||||
paginate: this.trpc.procedure
|
||||
.input(DepartmentSchema.paginate) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.departmentService.paginate(input);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,365 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { db, z, DepartmentSchema } from '@nicestack/common';
|
||||
|
||||
@Injectable()
|
||||
export class DepartmentService {
|
||||
/**
|
||||
* 获取某单位的所有子单位(平铺结构)。
|
||||
* @param deptId - 单位的唯一标识符。
|
||||
* @returns 包含所有子单位的数组,如果未传递deptId则返回undefined。
|
||||
*/
|
||||
async getFlatChildDepts(deptId: string) {
|
||||
if (!deptId) return undefined;
|
||||
|
||||
return await db.deptAncestry.findMany({
|
||||
where: { ancestorId: deptId },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取指定DOM下的对应name的单位
|
||||
* @param domId
|
||||
* @param name
|
||||
* @returns
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
async findByNameInDom(domId: string, name: string) {
|
||||
const subDepts = await db.deptAncestry.findMany({
|
||||
where: {
|
||||
ancestorId: domId,
|
||||
},
|
||||
include: {
|
||||
descendant: true,
|
||||
},
|
||||
});
|
||||
const dept = subDepts.find((item) => item.descendant.name === name);
|
||||
|
||||
return dept?.descendant;
|
||||
}
|
||||
/**
|
||||
* 获取某单位的所有父单位(平铺结构)。
|
||||
* @param deptId - 单位的唯一标识符。
|
||||
* @returns 包含所有父单位的数组,如果未传递deptId则返回undefined。
|
||||
*/
|
||||
async getFlatParentDepts(deptId: string) {
|
||||
if (!deptId) return undefined;
|
||||
|
||||
return await db.deptAncestry.findMany({
|
||||
where: { descendantId: deptId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某单位的所有子单位ID。
|
||||
* @param deptId - 单位的唯一标识符。
|
||||
* @returns 包含所有子单位ID的数组。
|
||||
*/
|
||||
async getAllChildDeptIds(deptId: string) {
|
||||
const res = await this.getFlatChildDepts(deptId);
|
||||
return res?.map((dept) => dept.descendantId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某单位的所有父单位ID。
|
||||
* @param deptId - 单位的唯一标识符。
|
||||
* @returns 包含所有父单位ID的数组。
|
||||
*/
|
||||
async getAllParentDeptIds(deptId: string) {
|
||||
const res = await this.getFlatParentDepts(deptId);
|
||||
return res?.map((dept) => dept.ancestorId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单位及其直接子单位的详细信息。
|
||||
* @param deptId - 要获取的单位的唯一标识符。
|
||||
* @returns 包含单位详细信息的对象,包括其子单位和员工信息。
|
||||
*/
|
||||
async getDepartmentDetails(deptId: string) {
|
||||
const department = await db.department.findUnique({
|
||||
where: { id: deptId },
|
||||
include: { children: true, deptStaffs: true },
|
||||
});
|
||||
|
||||
const childrenData = await db.deptAncestry.findMany({
|
||||
where: { ancestorId: deptId, relDepth: 1 },
|
||||
include: { descendant: { include: { children: true } } },
|
||||
});
|
||||
|
||||
const children = childrenData.map(({ descendant }) => ({
|
||||
id: descendant.id,
|
||||
name: descendant.name,
|
||||
order: descendant.order,
|
||||
parentId: descendant.parentId,
|
||||
hasChildren: Boolean(descendant.children?.length),
|
||||
childrenCount: descendant.children?.length || 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
id: department?.id,
|
||||
name: department?.name,
|
||||
order: department?.order,
|
||||
parentId: department?.parentId,
|
||||
children,
|
||||
staffs: department?.deptStaffs,
|
||||
hasChildren: !!children.length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某单位的所有直接子单位。
|
||||
* @param parentId - 父单位的唯一标识符,如果未传递则获取顶级单位。
|
||||
* @returns 包含所有直接子单位信息的数组。
|
||||
*/
|
||||
async getChildren(parentId?: string) {
|
||||
const departments = await db.department.findMany({
|
||||
where: { parentId: parentId ?? null },
|
||||
include: { children: true, deptStaffs: true },
|
||||
});
|
||||
|
||||
return departments.map((dept) => ({
|
||||
...dept,
|
||||
hasChildren: dept.children.length > 0,
|
||||
staffs: dept.deptStaffs,
|
||||
}));
|
||||
}
|
||||
async paginate(data: z.infer<typeof DepartmentSchema.paginate>) {
|
||||
const { page, pageSize, ids } = data;
|
||||
|
||||
const [items, totalCount] = await Promise.all([
|
||||
db.department.findMany({
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
where: {
|
||||
deletedAt: null,
|
||||
OR: [{ id: { in: ids } }],
|
||||
},
|
||||
include: { deptStaffs: true, parent: true },
|
||||
orderBy: { order: 'asc' },
|
||||
}),
|
||||
db.department.count({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
OR: [{ id: { in: ids } }],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return { items, totalCount };
|
||||
}
|
||||
async findMany(data: z.infer<typeof DepartmentSchema.findMany>) {
|
||||
const { keyword = '', ids } = data;
|
||||
|
||||
const departments = await db.department.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
OR: [{ name: { contains: keyword! } }, ids ? { id: { in: ids } } : {}],
|
||||
},
|
||||
include: { deptStaffs: true },
|
||||
orderBy: { order: 'asc' },
|
||||
take: 20,
|
||||
});
|
||||
|
||||
return departments.map((dept) => ({
|
||||
...dept,
|
||||
staffs: dept.deptStaffs,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有域内单位,根据查询条件筛选结果。
|
||||
* @param query - 可选的查询条件,用于模糊匹配单位名称。
|
||||
* @returns 包含符合条件的域内单位信息的数组。
|
||||
*/
|
||||
async getDomainDepartments(query?: string) {
|
||||
return await db.department.findMany({
|
||||
where: { isDomain: true, name: { contains: query } },
|
||||
take: 10,
|
||||
});
|
||||
}
|
||||
|
||||
async getDeptIdsByStaffIds(ids: string[]) {
|
||||
const staffs = await db.staff.findMany({
|
||||
where: { id: { in: ids } },
|
||||
});
|
||||
|
||||
return staffs.map((staff) => staff.deptId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的单位并管理DeptAncestry关系。
|
||||
* @param data - 用于创建新单位的数据。
|
||||
* @returns 新创建的单位对象。
|
||||
*/
|
||||
async create(data: z.infer<typeof DepartmentSchema.create>) {
|
||||
let newOrder = 0;
|
||||
|
||||
// 确定新单位的顺序
|
||||
const siblingDepartments = await db.department.findMany({
|
||||
where: { parentId: data.parentId ?? null },
|
||||
orderBy: { order: 'desc' },
|
||||
take: 1,
|
||||
});
|
||||
|
||||
if (siblingDepartments.length > 0) {
|
||||
newOrder = siblingDepartments[0].order + 1;
|
||||
}
|
||||
|
||||
// 根据计算的顺序创建新单位
|
||||
const newDepartment = await db.department.create({
|
||||
data: { ...data, order: newOrder },
|
||||
});
|
||||
|
||||
// 如果存在parentId,则更新DeptAncestry关系
|
||||
if (data.parentId) {
|
||||
const parentAncestries = await db.deptAncestry.findMany({
|
||||
where: { descendantId: data.parentId },
|
||||
orderBy: { relDepth: 'asc' },
|
||||
});
|
||||
|
||||
// 为新单位创建新的祖先记录
|
||||
const newAncestries = parentAncestries.map((ancestry) => ({
|
||||
ancestorId: ancestry.ancestorId,
|
||||
descendantId: newDepartment.id,
|
||||
relDepth: ancestry.relDepth + 1,
|
||||
}));
|
||||
|
||||
newAncestries.push({
|
||||
ancestorId: data.parentId,
|
||||
descendantId: newDepartment.id,
|
||||
relDepth: 1,
|
||||
});
|
||||
|
||||
await db.deptAncestry.createMany({ data: newAncestries });
|
||||
}
|
||||
|
||||
return newDepartment;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新现有单位,并在parentId更改时管理DeptAncestry关系。
|
||||
* @param data - 用于更新现有单位的数据。
|
||||
* @returns 更新后的单位对象。
|
||||
*/
|
||||
async update(data: z.infer<typeof DepartmentSchema.update>) {
|
||||
return await db.$transaction(async (transaction) => {
|
||||
const currentDepartment = await transaction.department.findUnique({
|
||||
where: { id: data.id },
|
||||
});
|
||||
if (!currentDepartment) throw new Error('Department not found');
|
||||
|
||||
const updatedDepartment = await transaction.department.update({
|
||||
where: { id: data.id },
|
||||
data: data,
|
||||
});
|
||||
|
||||
if (data.parentId !== currentDepartment.parentId) {
|
||||
await transaction.deptAncestry.deleteMany({
|
||||
where: { descendantId: data.id },
|
||||
});
|
||||
|
||||
if (data.parentId) {
|
||||
const parentAncestries = await transaction.deptAncestry.findMany({
|
||||
where: { descendantId: data.parentId },
|
||||
});
|
||||
|
||||
const newAncestries = parentAncestries.map((ancestry) => ({
|
||||
ancestorId: ancestry.ancestorId,
|
||||
descendantId: data.id,
|
||||
relDepth: ancestry.relDepth + 1,
|
||||
}));
|
||||
|
||||
newAncestries.push({
|
||||
ancestorId: data.parentId,
|
||||
descendantId: data.id,
|
||||
relDepth: 1,
|
||||
});
|
||||
|
||||
await transaction.deptAncestry.createMany({ data: newAncestries });
|
||||
}
|
||||
}
|
||||
|
||||
return updatedDepartment;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除现有单位并清理DeptAncestry关系。
|
||||
* @param data - 用于删除现有单位的数据。
|
||||
* @returns 删除的单位对象。
|
||||
*/
|
||||
async delete(data: z.infer<typeof DepartmentSchema.delete>) {
|
||||
const deletedDepartment = await db.department.update({
|
||||
where: { id: data.id },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
|
||||
await db.deptAncestry.deleteMany({
|
||||
where: { OR: [{ ancestorId: data.id }, { descendantId: data.id }] },
|
||||
});
|
||||
return deletedDepartment;
|
||||
}
|
||||
async getStaffsByDeptIds(ids: string[]) {
|
||||
const depts = await db.department.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: { deptStaffs: true },
|
||||
});
|
||||
return depts.flatMap((dept) => dept.deptStaffs);
|
||||
}
|
||||
/**
|
||||
* 获取指定部门及其所有子部门的员工。
|
||||
* @param deptIds - 要获取员工ID的部门ID数组。
|
||||
* @returns 包含所有员工ID的数组。
|
||||
*/
|
||||
async getAllStaffsByDepts(deptIds: string[]) {
|
||||
const allDeptIds = new Set(deptIds);
|
||||
for (const deptId of deptIds) {
|
||||
const childDeptIds = await this.getAllChildDeptIds(deptId);
|
||||
childDeptIds.forEach((id) => allDeptIds.add(id));
|
||||
}
|
||||
return await this.getStaffsByDeptIds(Array.from(allDeptIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门名称和域ID获取部门ID。
|
||||
*
|
||||
* @param {string} name - 部门名称。
|
||||
* @param {string} domainId - 域标识符。
|
||||
* @returns {Promise<string | null>} - 如果找到则返回部门ID,否则返回null。
|
||||
*/
|
||||
async getDeptIdByName(name: string, domainId: string): Promise<string | null> {
|
||||
const dept = await db.department.findFirst({
|
||||
where: {
|
||||
name,
|
||||
ancestors: {
|
||||
some: {
|
||||
ancestorId: domainId
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return dept ? dept.id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据部门名称列表和域ID获取多个部门的ID。
|
||||
*
|
||||
* @param {string[]} names - 部门名称列表。
|
||||
* @param {string} domainId - 域标识符。
|
||||
* @returns {Promise<Record<string, string | null>>} - 一个从部门名称到对应ID或null的记录。
|
||||
*/
|
||||
async getDeptIdsByNames(names: string[], domainId: string): Promise<Record<string, string | null>> {
|
||||
const result: Record<string, string | null> = {};
|
||||
|
||||
// 遍历每个部门名称并获取对应的部门ID
|
||||
for (const name of names) {
|
||||
// 使用之前定义的函数根据名称获取部门ID
|
||||
const deptId = await this.getDeptIdByName(name, domainId);
|
||||
result[name] = deptId;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({})
|
||||
export class StaffModule {}
|
|
@ -0,0 +1,53 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { StaffService } from './staff.service'; // Adjust the import path as necessary
|
||||
import { z, StaffSchema } from '@nicestack/common';
|
||||
|
||||
@Injectable()
|
||||
export class StaffRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly staffService: StaffService,
|
||||
) {}
|
||||
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.procedure
|
||||
.input(StaffSchema.create) // Assuming StaffSchema.create is the Zod schema for creating staff
|
||||
.mutation(async ({ input }) => {
|
||||
return await this.staffService.create(input);
|
||||
}),
|
||||
|
||||
update: this.trpc.procedure
|
||||
.input(StaffSchema.update) // Assuming StaffSchema.update is the Zod schema for updating staff
|
||||
.mutation(async ({ input }) => {
|
||||
return await this.staffService.update(input);
|
||||
}),
|
||||
|
||||
batchDelete: this.trpc.procedure
|
||||
.input(StaffSchema.batchDelete) // Assuming StaffSchema.batchDelete is the Zod schema for batch deleting staff
|
||||
.mutation(async ({ input }) => {
|
||||
return await this.staffService.batchDelete(input);
|
||||
}),
|
||||
|
||||
paginate: this.trpc.procedure
|
||||
.input(StaffSchema.paginate) // Define the input schema for pagination
|
||||
.query(async ({ input }) => {
|
||||
return await this.staffService.paginate(input);
|
||||
}),
|
||||
findByDept: this.trpc.procedure
|
||||
.input(StaffSchema.findByDept)
|
||||
.query(async ({ input }) => {
|
||||
return await this.staffService.findByDept(input);
|
||||
}),
|
||||
findMany: this.trpc.procedure
|
||||
.input(StaffSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.staffService.findMany(input);
|
||||
}),
|
||||
findUnique: this.trpc.procedure
|
||||
.input(StaffSchema.findUnique)
|
||||
.query(async ({ input }) => {
|
||||
return await this.staffService.findUnique(input);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HelloService } from './hello.service';
|
||||
import { StaffService } from './staff.service';
|
||||
|
||||
describe('HelloService', () => {
|
||||
let service: HelloService;
|
||||
describe('StaffService', () => {
|
||||
let service: StaffService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [HelloService],
|
||||
providers: [StaffService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<HelloService>(HelloService);
|
||||
service = module.get<StaffService>(StaffService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
|
@ -0,0 +1,178 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { db, ObjectType, Staff, StaffSchema, z } from '@nicestack/common';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { DepartmentService } from '../department/department.service';
|
||||
@Injectable()
|
||||
export class StaffService {
|
||||
constructor(private readonly departmentService: DepartmentService) { }
|
||||
|
||||
/**
|
||||
* 获取某一单位下所有staff的记录
|
||||
* @param deptId 单位的id
|
||||
* @returns 查到的staff记录
|
||||
*/
|
||||
async findByDept(data: z.infer<typeof StaffSchema.findByDept>) {
|
||||
const { deptId, domainId } = data;
|
||||
const childDepts = await this.departmentService.getAllChildDeptIds(deptId);
|
||||
const result = await db.staff.findMany({
|
||||
where: {
|
||||
deptId: { in: [...childDepts, deptId] },
|
||||
domainId,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 创建新的员工记录
|
||||
* @param data 员工创建信息
|
||||
* @returns 新创建的员工记录
|
||||
*/
|
||||
async create(data: z.infer<typeof StaffSchema.create>) {
|
||||
const { ...others } = data;
|
||||
|
||||
try {
|
||||
return await db.$transaction(async (transaction) => {
|
||||
// 获取当前最大order值
|
||||
const maxOrder = await transaction.staff.aggregate({
|
||||
_max: { order: true },
|
||||
});
|
||||
// 新员工的order值比现有最大order值大1
|
||||
const newOrder = (maxOrder._max.order ?? -1) + 1;
|
||||
// 创建新员工记录
|
||||
const newStaff = await transaction.staff.create({
|
||||
data: { ...others, order: newOrder },
|
||||
include: { domain: true, department: true },
|
||||
});
|
||||
return newStaff;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create staff:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 更新员工记录
|
||||
* @param data 包含id和其他更新字段的对象
|
||||
* @returns 更新后的员工记录
|
||||
*/
|
||||
async update(data: z.infer<typeof StaffSchema.update>) {
|
||||
const { id, ...others } = data;
|
||||
try {
|
||||
return await db.$transaction(async (transaction) => {
|
||||
// 更新员工记录
|
||||
const updatedStaff = await transaction.staff.update({
|
||||
where: { id },
|
||||
data: others,
|
||||
include: { domain: true, department: true },
|
||||
});
|
||||
return updatedStaff;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update staff:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 批量删除员工记录(软删除)
|
||||
* @param data 包含要删除的员工ID数组的对象
|
||||
* @returns 删除操作结果,包括删除的记录数
|
||||
*/
|
||||
async batchDelete(data: z.infer<typeof StaffSchema.batchDelete>) {
|
||||
const { ids } = data;
|
||||
|
||||
if (!ids || ids.length === 0) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'No IDs provided for deletion.',
|
||||
});
|
||||
}
|
||||
const deletedStaffs = await db.staff.updateMany({
|
||||
where: { id: { in: ids } },
|
||||
data: { deletedAt: new Date() },
|
||||
});
|
||||
if (!deletedStaffs.count) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'No taxonomies were found with the provided IDs.',
|
||||
});
|
||||
}
|
||||
return { success: true, count: deletedStaffs.count };
|
||||
}
|
||||
/**
|
||||
* 分页查询员工
|
||||
* @param data 包含分页参数、域ID和部门ID的对象
|
||||
* @returns 员工列表及总记录数
|
||||
*/
|
||||
async paginate(data: z.infer<typeof StaffSchema.paginate>) {
|
||||
const { page, pageSize, domainId, deptId, ids } = data;
|
||||
const childDepts = await this.departmentService.getAllChildDeptIds(deptId);
|
||||
const [items, totalCount] = await Promise.all([
|
||||
db.staff.findMany({
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: { order: 'asc' },
|
||||
where: {
|
||||
id: ids ? { in: ids } : undefined,
|
||||
deletedAt: null,
|
||||
domainId,
|
||||
deptId: deptId ? { in: [...childDepts, deptId] } : undefined,
|
||||
},
|
||||
include: { domain: true, department: true },
|
||||
}),
|
||||
db.staff.count({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
domainId,
|
||||
deptId: deptId ? { in: [...childDepts, deptId] } : undefined,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const processedItems = await Promise.all(
|
||||
items.map((item) => this.genStaffDto(item)),
|
||||
);
|
||||
return { items: processedItems, totalCount };
|
||||
}
|
||||
/**
|
||||
* 根据关键词或ID集合查找员工
|
||||
* @param data 包含关键词、域ID和ID集合的对象
|
||||
* @returns 匹配的员工记录列表
|
||||
*/
|
||||
async findMany(data: z.infer<typeof StaffSchema.findMany>) {
|
||||
const { keyword, domainId, ids } = data;
|
||||
|
||||
return await db.staff.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
domainId,
|
||||
OR: [
|
||||
{ username: { contains: keyword } },
|
||||
{
|
||||
id: { in: ids },
|
||||
},
|
||||
],
|
||||
},
|
||||
orderBy: { order: 'asc' },
|
||||
take: 10,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 生成员工的数据传输对象(DTO)
|
||||
* @param staff 员工记录
|
||||
* @returns 含角色ID列表的员工DTO
|
||||
*/
|
||||
private async genStaffDto(staff: Staff) {
|
||||
const roleMaps = await db.roleMap.findMany({
|
||||
where: {
|
||||
domainId: staff.domainId,
|
||||
objectId: staff.id,
|
||||
objectType: ObjectType.STAFF,
|
||||
},
|
||||
include: { role: true },
|
||||
});
|
||||
const roleIds = roleMaps.map((roleMap) => roleMap.role.id);
|
||||
return { ...staff, roleIds };
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TasksService } from './tasks.service';
|
||||
import { InitModule } from '@server/init/init.module';
|
||||
@Module({
|
||||
imports: [InitModule],
|
||||
providers: [TasksService]
|
||||
})
|
||||
export class TasksModule { }
|
|
@ -0,0 +1,27 @@
|
|||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { InitService } from '@server/init/init.service';
|
||||
import { CronJob } from 'cron';
|
||||
|
||||
@Injectable()
|
||||
export class TasksService implements OnModuleInit {
|
||||
private readonly logger = new Logger(TasksService.name);
|
||||
|
||||
constructor(
|
||||
private readonly schedulerRegistry: SchedulerRegistry,
|
||||
private readonly initService: InitService,
|
||||
) { }
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log('Main node launch');
|
||||
await this.initService.init();
|
||||
|
||||
const handleCronJob = new CronJob('0 * * * *', () => {
|
||||
this.logger.log('cron job test');
|
||||
});
|
||||
|
||||
this.schedulerRegistry.addCronJob('cronJob', handleCronJob);
|
||||
this.logger.log('Start cron job');
|
||||
handleCronJob.start();
|
||||
}
|
||||
}
|
|
@ -1,21 +1,19 @@
|
|||
import { INestApplication, Injectable } from '@nestjs/common';
|
||||
import { AuthRouter } from '@server/auth/auth.router';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import * as trpcExpress from '@trpc/server/adapters/express';
|
||||
import { HelloRouter } from '@server/hello/hello.router';
|
||||
|
||||
@Injectable()
|
||||
export class TrpcRouter {
|
||||
constructor(private readonly trpc: TrpcService, private readonly hello: HelloRouter) { }
|
||||
|
||||
constructor(private readonly trpc: TrpcService, private readonly auth: AuthRouter) { }
|
||||
appRouter = this.trpc.router({
|
||||
hello: this.hello.router
|
||||
auth: this.auth.router
|
||||
});
|
||||
|
||||
async applyMiddleware(app: INestApplication) {
|
||||
app.use(
|
||||
`/trpc`,
|
||||
trpcExpress.createExpressMiddleware({
|
||||
router: this.appRouter,
|
||||
createContext: this.trpc.createContext
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,59 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { initTRPC } from '@trpc/server';
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import superjson from 'superjson-cjs';
|
||||
import * as trpcExpress from '@trpc/server/adapters/express';
|
||||
import { env } from '@server/env';
|
||||
import { db, Staff, TokenPayload } from "@nicestack/common"
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
type Context = Awaited<ReturnType<TrpcService['createContext']>>;
|
||||
@Injectable()
|
||||
export class TrpcService {
|
||||
trpc = initTRPC.create({
|
||||
transformer: superjson
|
||||
constructor(private readonly jwtService: JwtService) { }
|
||||
async createContext({
|
||||
req,
|
||||
res,
|
||||
}: trpcExpress.CreateExpressContextOptions) {
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
let tokenData: TokenPayload | undefined = undefined;
|
||||
let staff: Staff | undefined = undefined;
|
||||
if (token) {
|
||||
try {
|
||||
// Verify JWT token and extract tokenData
|
||||
tokenData = await this.jwtService.verifyAsync(token, { secret: env.JWT_SECRET }) as TokenPayload;
|
||||
if (tokenData) {
|
||||
// Fetch staff details from the database using tokenData.id
|
||||
staff = await db.staff.findUnique({ where: { id: tokenData.id } });
|
||||
if (!staff) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: "User not found" });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Enhanced error handling for invalid session data or token verification failure
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: "Invalid session data or token" });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
staff,
|
||||
};
|
||||
};
|
||||
trpc = initTRPC.context<Context>().create({
|
||||
transformer: superjson,
|
||||
});
|
||||
procedure = this.trpc.procedure;
|
||||
router = this.trpc.router;
|
||||
mergeRouters = this.trpc.mergeRouters;
|
||||
// Define a protected procedure that ensures the user is authenticated
|
||||
protectProcedure = this.procedure.use(async ({ ctx, next }) => {
|
||||
if (!ctx.staff) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: "Unauthorized request" });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
// User value is confirmed to be non-null at this point
|
||||
staff: ctx.staff,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"strictNullChecks": false,
|
||||
// "baseUrl": "./",
|
||||
// "incremental": true,
|
||||
// "skipLibCheck": true,
|
||||
|
|
|
@ -20,7 +20,7 @@ services:
|
|||
- ./volumes/minio:/minio_data
|
||||
environment:
|
||||
- MINIO_ACCESS_KEY=minioadmin
|
||||
- MINIO_SECRET_KEY=minioadmin
|
||||
- MINIO_JWT_SECRET=minioadmin
|
||||
command: minio server /minio_data --console-address ":9001" -address ":9000"
|
||||
healthcheck:
|
||||
test:
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
// 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")
|
||||
}
|
||||
|
||||
model Taxonomy {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
deletedAt DateTime?
|
||||
terms Term[]
|
||||
order Int
|
||||
|
||||
@@index([order, deletedAt])
|
||||
}
|
||||
|
||||
model Relation {
|
||||
id String @id @default(uuid())
|
||||
aId String
|
||||
bId String
|
||||
aType String
|
||||
bType String
|
||||
relationType String
|
||||
createdAt DateTime? @default(now())
|
||||
|
||||
@@unique([aId, bId, aType, bType, relationType])
|
||||
@@map("relations")
|
||||
}
|
||||
|
||||
model Term {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
taxonomy Taxonomy? @relation(fields: [taxonomyId], references: [id])
|
||||
taxonomyId String?
|
||||
order Int
|
||||
description String?
|
||||
parentId String?
|
||||
parent Term? @relation("ChildParent", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children Term[] @relation("ChildParent")
|
||||
ancestors TermAncestry[] @relation("DescendantToAncestor")
|
||||
descendants TermAncestry[] @relation("AncestorToDescendant")
|
||||
|
||||
domainId String?
|
||||
domain Department? @relation("TermDom", fields: [domainId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
createdBy String
|
||||
createdStaff Staff? @relation(fields: [staffId], references: [id])
|
||||
staffId String?
|
||||
|
||||
@@index([name]) // 对name字段建立索引,以加快基于name的查找速度
|
||||
@@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度
|
||||
@@map("terms")
|
||||
}
|
||||
|
||||
model TermAncestry {
|
||||
id String @id @default(uuid())
|
||||
ancestorId String
|
||||
descendantId String
|
||||
relDepth Int
|
||||
ancestor Term @relation("AncestorToDescendant", fields: [ancestorId], references: [id], onDelete: Cascade)
|
||||
descendant Term @relation("DescendantToAncestor", fields: [descendantId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime? @default(now())
|
||||
}
|
||||
|
||||
model Comment {
|
||||
id String @id @default(uuid())
|
||||
style String
|
||||
link String?
|
||||
title String?
|
||||
content String
|
||||
attachments String[] @default([])
|
||||
createdAt DateTime? @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
createdBy String?
|
||||
|
||||
createdStaff Staff? @relation(fields: [createdBy], references: [id])
|
||||
|
||||
@@map("comments")
|
||||
}
|
||||
|
||||
model Staff {
|
||||
id String @id @default(uuid())
|
||||
username String @unique
|
||||
password String
|
||||
phoneNumber String? @unique
|
||||
domainId String?
|
||||
deptId String?
|
||||
domain Department? @relation("DomainStaff", fields: [domainId], references: [id])
|
||||
department Department? @relation("DeptStaff", fields: [deptId], references: [id])
|
||||
registerToken String?
|
||||
order Int
|
||||
deletedAt DateTime?
|
||||
system Boolean? @default(false)
|
||||
comments Comment[]
|
||||
terms Term[]
|
||||
refreshTokens RefreshToken[]
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(uuid())
|
||||
token String @unique
|
||||
staffId String
|
||||
staff Staff @relation(fields: [staffId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("refreshTokens")
|
||||
}
|
||||
|
||||
model Department {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
order Int
|
||||
ancestors DeptAncestry[] @relation("DescendantToAncestor")
|
||||
descendants DeptAncestry[] @relation("AncestorToDescendant")
|
||||
parentId String? @map("parentId")
|
||||
parent Department? @relation("ChildParent", fields: [parentId], references: [id])
|
||||
children Department[] @relation("ChildParent")
|
||||
domainTerms Term[] @relation("TermDom")
|
||||
deletedAt DateTime?
|
||||
isDomain Boolean? @default(false)
|
||||
domainStaffs Staff[] @relation("DomainStaff")
|
||||
deptStaffs Staff[] @relation("DeptStaff")
|
||||
}
|
||||
|
||||
model DeptAncestry {
|
||||
ancestorId String
|
||||
descendantId String
|
||||
relDepth Int
|
||||
ancestor Department @relation("AncestorToDescendant", fields: [ancestorId], references: [id])
|
||||
descendant Department @relation("DescendantToAncestor", fields: [descendantId], references: [id])
|
||||
|
||||
@@id([descendantId, ancestorId])
|
||||
@@index([ancestorId]) // 对ancestorId字段建立索引,以加快基于ancestorId的查找速度
|
||||
@@index([descendantId]) // 对descendantId字段建立索引,以加快基于descendantId的查找速度
|
||||
}
|
||||
|
||||
model RoleMap {
|
||||
id String @id @default(uuid())
|
||||
objectId String
|
||||
roleId String
|
||||
domainId String?
|
||||
objectType String
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
permissions String[] @default([])
|
||||
roleMaps RoleMap[]
|
||||
deletedAt DateTime?
|
||||
system Boolean? @default(false)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { RolePerms } from "./enum";
|
||||
|
||||
export const InitRoles: { name: string, permissions: string[], system?: boolean }[] = [
|
||||
{
|
||||
name: "基层",
|
||||
permissions: [
|
||||
RolePerms.CREATE_TROUBLE,
|
||||
RolePerms.CREATE_WORKPROGRESS,
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "机关",
|
||||
permissions: [
|
||||
RolePerms.CREATE_TROUBLE,
|
||||
RolePerms.CREATE_WORKPROGRESS,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "领导",
|
||||
permissions: [
|
||||
RolePerms.READ_DOM_TROUBLE,
|
||||
RolePerms.CREATE_INSTRUCTION,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "域管理员",
|
||||
permissions: Object.keys(RolePerms).filter(
|
||||
(perm) =>
|
||||
![
|
||||
RolePerms.READ_ANY_CHART,
|
||||
RolePerms.READ_ANY_TROUBLE,
|
||||
RolePerms.READ_ANY_TERM,
|
||||
RolePerms.PROCESS_ANY_ASSESSMENT,
|
||||
RolePerms.PROCESS_ANY_TROUBLE,
|
||||
RolePerms.EDIT_ROOT_OPTION,
|
||||
RolePerms.EDIT_ANY_TERM,
|
||||
RolePerms.EDIT_ANY_TROUBLE,
|
||||
RolePerms.EDIT_ANY_ASSESSMENT,
|
||||
RolePerms.DELETE_ANY_TROUBLE,
|
||||
RolePerms.DELETE_ANY_TERM,
|
||||
RolePerms.DELETE_ANY_ASSESSMENT,
|
||||
].includes(perm as any)
|
||||
) as RolePerms[],
|
||||
},
|
||||
{
|
||||
name: "根管理员",
|
||||
permissions: Object.keys(RolePerms) as RolePerms[],
|
||||
},
|
||||
];
|
||||
export const InitTaxonomies: { name: string }[] = [{
|
||||
name: '分类'
|
||||
},
|
||||
{
|
||||
name: '研判单元'
|
||||
}]
|
|
@ -0,0 +1,57 @@
|
|||
export enum ObjectType {
|
||||
DEPARTMENT = "DEPARTMENT",
|
||||
STAFF = "STAFF",
|
||||
COMMENT = "COMMENT",
|
||||
TERM = "TERM",
|
||||
}
|
||||
export enum RelationType {
|
||||
WATCH = "WATCH",
|
||||
READED = "READED",
|
||||
MESSAGE = "MESSAGE",
|
||||
}
|
||||
export enum RolePerms {
|
||||
// Create Permissions 创建权限
|
||||
CREATE_ALERT = "CREATE_ALERT", // 创建警报
|
||||
CREATE_INSTRUCTION = "CREATE_INSTRUCTION", // 创建指令
|
||||
CREATE_TROUBLE = "CREATE_TROUBLE", // 创建问题
|
||||
CREATE_WORKPROGRESS = "CREATE_WORKPROGRESS", // 创建工作进度
|
||||
CREATE_ASSESSMENT = "CREATE_ASSESSMENT", // 创建评估
|
||||
CREATE_TERM = "CREATE_TERM", // 创建术语
|
||||
|
||||
// Read Permissions 读取权限
|
||||
READ_ANY_TROUBLE = "READ_ANY_TROUBLE", // 读取任何问题
|
||||
READ_DOM_TROUBLE = "READ_DOM_TROUBLE", // 读取领域问题
|
||||
READ_ANY_CHART = "READ_ANY_CHART", // 读取任何图表
|
||||
READ_DOM_CHART = "READ_DOM_CHART", // 读取领域图表
|
||||
READ_ANY_ASSESSMENT = "READ_ANY_ASSESSMENT", // 读取任何评估
|
||||
READ_DOM_ASSESSMENT = "READ_DOM_ASSESSMENT", // 读取领域评估
|
||||
READ_ANY_TERM = "READ_ANY_TERM", // 读取任何术语
|
||||
READ_DOM_TERM = "READ_DOM_TERM", // 读取领域术语
|
||||
|
||||
// Edit Permissions 编辑权限
|
||||
EDIT_DOM_TROUBLE = "EDIT_DOM_TROUBLE", // 编辑领域问题
|
||||
EDIT_ANY_TROUBLE = "EDIT_ANY_TROUBLE", // 编辑任何问题
|
||||
EDIT_DOM_ROLE = "EDIT_DOM_ROLE", // 编辑领域角色
|
||||
EDIT_ROOT_OPTION = "EDIT_ROOT_OPTION", // 编辑根选项
|
||||
EDIT_DOM_ASSESSMENT = "EDIT_DOM_ASSESSMENT", // 编辑领域评估
|
||||
EDIT_ANY_ASSESSMENT = "EDIT_ANY_ASSESSMENT", // 编辑任何评估
|
||||
EDIT_DOM_TERM = "EDIT_DOM_TERM", // 编辑领域术语
|
||||
EDIT_ANY_TERM = "EDIT_ANY_TERM", // 编辑任何术语
|
||||
|
||||
// Delete Permissions 删除权限
|
||||
DELETE_DOM_TROUBLE = "DELETE_DOM_TROUBLE", // 删除领域问题
|
||||
DELETE_ANY_TROUBLE = "DELETE_ANY_TROUBLE", // 删除任何问题
|
||||
DELETE_DOM_ASSESSMENT = "DELETE_DOM_ASSESSMENT", // 删除领域评估
|
||||
DELETE_ANY_ASSESSMENT = "DELETE_ANY_ASSESSMENT", // 删除任何评估
|
||||
DELETE_DOM_TERM = "DELETE_DOM_TERM", // 删除领域术语
|
||||
DELETE_ANY_TERM = "DELETE_ANY_TERM", // 删除任何术语
|
||||
|
||||
// Process Permissions 处理权限
|
||||
PROCESS_DOM_TROUBLE = "PROCESS_DOM_TROUBLE", // 处理领域问题
|
||||
PROCESS_ANY_TROUBLE = "PROCESS_ANY_TROUBLE", // 处理任何问题
|
||||
PROCESS_DOM_ASSESSMENT = "PROCESS_DOM_ASSESSMENT", // 处理领域评估
|
||||
PROCESS_ANY_ASSESSMENT = "PROCESS_ANY_ASSESSMENT", // 处理任何评估
|
||||
|
||||
// Audit Permissions 审核权限
|
||||
AUDIT_TROUBLE = "AUDIT_TROUBLE", // 审核问题
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
export * from "@prisma/client"
|
||||
export * from "zod"
|
||||
export * from "./db"
|
||||
export * from '@prisma/client';
|
||||
|
||||
export * from "./schema"
|
||||
export * from "./schema"
|
||||
export * from "./enum"
|
||||
export * from "./type"
|
||||
export * from "./utils"
|
||||
export * from "./constants"
|
|
@ -1 +1,92 @@
|
|||
export { }
|
||||
import { z } from "zod"
|
||||
export const AuthSchema = {
|
||||
signInRequset: z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
}),
|
||||
signUpRequest: z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
}),
|
||||
changePassword: z.object({
|
||||
username: z.string(),
|
||||
oldPassword: z.string(),
|
||||
newPassword: z.string(),
|
||||
}),
|
||||
refreshTokenRequest: z.object({
|
||||
refreshToken: z.string(),
|
||||
}),
|
||||
logoutRequest: z.object({
|
||||
refreshToken: z.string(),
|
||||
}),
|
||||
};
|
||||
export const StaffSchema = {
|
||||
create: z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
domainId: z.string().nullish(),
|
||||
}),
|
||||
update: z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
password: z.string().nullish(),
|
||||
domainId: z.string().nullish(),
|
||||
deptId: z.string().nullish(),
|
||||
phoneNumber: z.string().nullish(),
|
||||
order: z.number().nullish(),
|
||||
registerToken: z.string().nullish(),
|
||||
}),
|
||||
delete: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
batchDelete: z.object({
|
||||
ids: z.array(z.string()),
|
||||
}),
|
||||
findByDept: z.object({
|
||||
deptId: z.string(),
|
||||
domainId: z.string().nullish(),
|
||||
}),
|
||||
findMany: z.object({
|
||||
keyword: z.string().nullish(),
|
||||
domainId: z.string().nullish(),
|
||||
ids: z.array(z.string()).nullish(),
|
||||
}),
|
||||
findUnique: z.object({
|
||||
|
||||
id: z.string().nullish(),
|
||||
}),
|
||||
paginate: z.object({
|
||||
page: z.number(),
|
||||
pageSize: z.number(),
|
||||
domainId: z.string().nullish(),
|
||||
deptId: z.string().nullish(),
|
||||
ids: z.array(z.string()).nullish(),
|
||||
}),
|
||||
};
|
||||
export const DepartmentSchema = {
|
||||
create: z.object({
|
||||
name: z.string(),
|
||||
parentId: z.string().nullish(),
|
||||
isDomain: z.boolean().nullish(),
|
||||
}),
|
||||
update: z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
parentId: z.string().nullish(),
|
||||
deletedAt: z.date().nullish(),
|
||||
order: z.number().nullish(),
|
||||
isDomain: z.boolean().nullish(),
|
||||
}),
|
||||
delete: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
findMany: z.object({
|
||||
keyword: z.string().nullish(),
|
||||
ids: z.array(z.string()).nullish(),
|
||||
}),
|
||||
paginate: z.object({
|
||||
page: z.number(),
|
||||
pageSize: z.number(),
|
||||
ids: z.array(z.string()).nullish(),
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { Department, Staff } from "@prisma/client";
|
||||
|
||||
export interface DataNode {
|
||||
title: any;
|
||||
key: string;
|
||||
hasChildren?: boolean;
|
||||
children?: DataNode[];
|
||||
value: string;
|
||||
data?: any;
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
export type StaffDto = Staff & {
|
||||
domain?: Department;
|
||||
department?: Department;
|
||||
};
|
||||
export interface TokenPayload {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import { Staff } from "@prisma/client";
|
||||
import { DataNode } from "./type";
|
||||
export function findNodeByKey(
|
||||
nodes: DataNode[],
|
||||
targetKey: string
|
||||
): DataNode | null {
|
||||
let result: DataNode | null = null;
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.key === targetKey) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
result = findNodeByKey(node.children, targetKey);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
export function findStaffById(
|
||||
nodes: DataNode[],
|
||||
staffId: string
|
||||
): Staff | null {
|
||||
for (const node of nodes) {
|
||||
// 在当前节点的staffs数组中查找
|
||||
const foundStaff = node?.data?.staffs.find(
|
||||
(staff: Staff) => staff.id === staffId
|
||||
);
|
||||
if (foundStaff) {
|
||||
return foundStaff;
|
||||
}
|
||||
|
||||
// 如果当前节点的staffs数组中没有找到,则递归在子节点中查找
|
||||
if (node.children) {
|
||||
const foundInChildren = findStaffById(node.children, staffId);
|
||||
if (foundInChildren) {
|
||||
return foundInChildren;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果在所有节点及其子节点中都没有找到,返回null
|
||||
return null;
|
||||
}
|
||||
interface MappingConfig {
|
||||
titleField?: string;
|
||||
keyField?: string;
|
||||
valueField?: string;
|
||||
hasChildrenField?: string; // Optional, in case the structure has nested items
|
||||
childrenField?: string;
|
||||
}
|
||||
|
||||
export function mapToDataNodes(
|
||||
inputArray: any[],
|
||||
config: MappingConfig = {}
|
||||
): DataNode[] {
|
||||
const {
|
||||
titleField = "title",
|
||||
keyField = "key",
|
||||
valueField = "value",
|
||||
hasChildrenField = "hasChildren",
|
||||
childrenField = "children"
|
||||
} = config;
|
||||
|
||||
return inputArray.map((item) => {
|
||||
const hasChildren = item[hasChildrenField] || false;
|
||||
const children = item[childrenField]
|
||||
return {
|
||||
title: item[titleField] || "",
|
||||
key: item[keyField] || "",
|
||||
value: item[valueField] || null,
|
||||
data: item,
|
||||
children: children
|
||||
? mapToDataNodes(children, { titleField, keyField, valueField, hasChildrenField, childrenField })
|
||||
: undefined,
|
||||
hasChildren
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并两个数组并去重。
|
||||
*
|
||||
* 该函数将两个输入数组的元素合并为一个数组,
|
||||
* 并确保结果数组中没有重复的元素。元素的顺序根据它们首次出现的顺序保留。
|
||||
*
|
||||
* @template T - 输入数组中元素的类型。
|
||||
* @param {T[]} array1 - 要合并的第一个数组。
|
||||
* @param {T[]} array2 - 要合并的第二个数组。
|
||||
* @returns {T[]} 包含来自两个输入数组的唯一元素的新数组。
|
||||
*
|
||||
* @example
|
||||
* const array1 = [1, 2, 3, 4];
|
||||
* const array2 = [3, 4, 5, 6];
|
||||
* const result = mergeAndDeduplicate(array1, array2);
|
||||
* console.log(result); // 输出: [1, 2, 3, 4, 5, 6]
|
||||
*/
|
||||
export function mergeAndDeduplicate<T = string>(array1: T[], array2: T[]): T[] {
|
||||
const set = new Set([...array1, ...array2]);
|
||||
return Array.from(set);
|
||||
}
|
||||
|
Loading…
Reference in New Issue