add
This commit is contained in:
parent
9075be6046
commit
678ad87c2f
|
@ -23,12 +23,12 @@ import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true, // 全局可用
|
isGlobal: true, // 全局可用
|
||||||
envFilePath: '.env'
|
envFilePath: '.env',
|
||||||
}),
|
}),
|
||||||
ScheduleModule.forRoot(),
|
ScheduleModule.forRoot(),
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
global: true,
|
global: true,
|
||||||
secret: env.JWT_SECRET
|
secret: env.JWT_SECRET,
|
||||||
}),
|
}),
|
||||||
WebSocketModule,
|
WebSocketModule,
|
||||||
TrpcModule,
|
TrpcModule,
|
||||||
|
@ -43,11 +43,13 @@ import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
|
||||||
CollaborationModule,
|
CollaborationModule,
|
||||||
RealTimeModule,
|
RealTimeModule,
|
||||||
UploadModule,
|
UploadModule,
|
||||||
SystemLogModule
|
SystemLogModule,
|
||||||
],
|
],
|
||||||
providers: [{
|
providers: [
|
||||||
|
{
|
||||||
provide: APP_FILTER,
|
provide: APP_FILTER,
|
||||||
useClass: ExceptionsFilter,
|
useClass: ExceptionsFilter,
|
||||||
}],
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|
|
@ -43,8 +43,9 @@ export class AuthController {
|
||||||
authorization,
|
authorization,
|
||||||
};
|
};
|
||||||
|
|
||||||
const authResult =
|
const authResult = await this.authService.validateFileRequest(
|
||||||
await this.authService.validateFileRequest(fileRequest);
|
fileRequest,
|
||||||
|
);
|
||||||
if (!authResult.isValid) {
|
if (!authResult.isValid) {
|
||||||
// 使用枚举类型进行错误处理
|
// 使用枚举类型进行错误处理
|
||||||
switch (authResult.error) {
|
switch (authResult.error) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { extractTokenFromHeader } from './utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
constructor(private jwtService: JwtService) { }
|
constructor(private jwtService: JwtService) {}
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const token = extractTokenFromHeader(request);
|
const token = extractTokenFromHeader(request);
|
||||||
|
@ -20,18 +20,13 @@ export class AuthGuard implements CanActivate {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const payload: JwtPayload = await this.jwtService.verifyAsync(
|
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
|
||||||
token,
|
secret: env.JWT_SECRET,
|
||||||
{
|
});
|
||||||
secret: env.JWT_SECRET
|
|
||||||
}
|
|
||||||
);
|
|
||||||
request['user'] = payload;
|
request['user'] = payload;
|
||||||
} catch {
|
} catch {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -8,12 +8,8 @@ import { SessionService } from './session.service';
|
||||||
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StaffModule, RoleMapModule],
|
imports: [StaffModule, RoleMapModule],
|
||||||
providers: [
|
providers: [AuthService, TrpcService, DepartmentService, SessionService],
|
||||||
AuthService,
|
|
||||||
TrpcService,
|
|
||||||
DepartmentService,
|
|
||||||
SessionService],
|
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule { }
|
export class AuthModule {}
|
||||||
|
|
|
@ -29,16 +29,21 @@ export class SessionService {
|
||||||
const sessionInfo: SessionInfo = {
|
const sessionInfo: SessionInfo = {
|
||||||
session_id: uuidv4(),
|
session_id: uuidv4(),
|
||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
access_token_expires_at: Date.now() + expirationConfig.accessTokenExpirationMs,
|
access_token_expires_at:
|
||||||
|
Date.now() + expirationConfig.accessTokenExpirationMs,
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
refresh_token_expires_at: Date.now() + expirationConfig.refreshTokenExpirationMs,
|
refresh_token_expires_at:
|
||||||
|
Date.now() + expirationConfig.refreshTokenExpirationMs,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL);
|
await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL);
|
||||||
return sessionInfo;
|
return sessionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSession(userId: string, sessionId: string): Promise<SessionInfo | null> {
|
async getSession(
|
||||||
|
userId: string,
|
||||||
|
sessionId: string,
|
||||||
|
): Promise<SessionInfo | null> {
|
||||||
const sessionData = await redis.get(this.getSessionKey(userId, sessionId));
|
const sessionData = await redis.get(this.getSessionKey(userId, sessionId));
|
||||||
return sessionData ? JSON.parse(sessionData) : null;
|
return sessionData ? JSON.parse(sessionData) : null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ export interface TokenConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileAuthResult {
|
export interface FileAuthResult {
|
||||||
isValid: boolean
|
isValid: boolean;
|
||||||
userId?: string
|
userId?: string;
|
||||||
resourceType?: string
|
resourceType?: string;
|
||||||
error?: string
|
error?: string;
|
||||||
}
|
}
|
||||||
export interface FileRequest {
|
export interface FileRequest {
|
||||||
originalUri: string;
|
originalUri: string;
|
||||||
|
@ -20,12 +20,12 @@ export interface FileRequest {
|
||||||
method: string;
|
method: string;
|
||||||
queryParams: string;
|
queryParams: string;
|
||||||
host: string;
|
host: string;
|
||||||
authorization: string
|
authorization: string;
|
||||||
}
|
}
|
||||||
export enum FileValidationErrorType {
|
export enum FileValidationErrorType {
|
||||||
INVALID_URI = 'INVALID_URI',
|
INVALID_URI = 'INVALID_URI',
|
||||||
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
||||||
AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED',
|
AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED',
|
||||||
INVALID_TOKEN = 'INVALID_TOKEN',
|
INVALID_TOKEN = 'INVALID_TOKEN',
|
||||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
|
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
|
||||||
}
|
}
|
|
@ -11,7 +11,7 @@ import { env } from '@server/env';
|
||||||
import { redis } from '@server/utils/redis/redis.service';
|
import { redis } from '@server/utils/redis/redis.service';
|
||||||
import EventBus from '@server/utils/event-bus';
|
import EventBus from '@server/utils/event-bus';
|
||||||
import { RoleMapService } from '@server/models/rbac/rolemap.service';
|
import { RoleMapService } from '@server/models/rbac/rolemap.service';
|
||||||
import { Request } from "express"
|
import { Request } from 'express';
|
||||||
interface ProfileResult {
|
interface ProfileResult {
|
||||||
staff: UserProfile | undefined;
|
staff: UserProfile | undefined;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
@ -22,9 +22,11 @@ interface TokenVerifyResult {
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
export function extractTokenFromHeader(request: Request): string | undefined {
|
export function extractTokenFromHeader(request: Request): string | undefined {
|
||||||
return extractTokenFromAuthorization(request.headers.authorization)
|
return extractTokenFromAuthorization(request.headers.authorization);
|
||||||
}
|
}
|
||||||
export function extractTokenFromAuthorization(authorization: string): string | undefined {
|
export function extractTokenFromAuthorization(
|
||||||
|
authorization: string,
|
||||||
|
): string | undefined {
|
||||||
const [type, token] = authorization?.split(' ') ?? [];
|
const [type, token] = authorization?.split(' ') ?? [];
|
||||||
return type === 'Bearer' ? token : undefined;
|
return type === 'Bearer' ? token : undefined;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +42,7 @@ export class UserProfileService {
|
||||||
this.jwtService = new JwtService();
|
this.jwtService = new JwtService();
|
||||||
this.departmentService = new DepartmentService();
|
this.departmentService = new DepartmentService();
|
||||||
this.roleMapService = new RoleMapService(this.departmentService);
|
this.roleMapService = new RoleMapService(this.departmentService);
|
||||||
EventBus.on("dataChanged", ({ type, data }) => {
|
EventBus.on('dataChanged', ({ type, data }) => {
|
||||||
if (type === ObjectType.STAFF) {
|
if (type === ObjectType.STAFF) {
|
||||||
// 确保 data 是数组,如果不是则转换为数组
|
// 确保 data 是数组,如果不是则转换为数组
|
||||||
const dataArray = Array.isArray(data) ? data : [data];
|
const dataArray = Array.isArray(data) ? data : [data];
|
||||||
|
@ -51,7 +53,6 @@ export class UserProfileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
public getProfileCacheKey(id: string) {
|
public getProfileCacheKey(id: string) {
|
||||||
return `user-profile-${id}`;
|
return `user-profile-${id}`;
|
||||||
|
@ -175,9 +176,7 @@ export class UserProfileService {
|
||||||
staff.deptId
|
staff.deptId
|
||||||
? this.departmentService.getDescendantIdsInDomain(staff.deptId)
|
? this.departmentService.getDescendantIdsInDomain(staff.deptId)
|
||||||
: [],
|
: [],
|
||||||
staff.deptId
|
staff.deptId ? this.departmentService.getAncestorIds([staff.deptId]) : [],
|
||||||
? this.departmentService.getAncestorIds([staff.deptId])
|
|
||||||
: [],
|
|
||||||
this.roleMapService.getPermsForObject({
|
this.roleMapService.getPermsForObject({
|
||||||
domainId: staff.domainId,
|
domainId: staff.domainId,
|
||||||
staffId: staff.id,
|
staffId: staff.id,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export const env: { JWT_SECRET: string } = {
|
export const env: { JWT_SECRET: string } = {
|
||||||
JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA='
|
JWT_SECRET:
|
||||||
}
|
process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA=',
|
||||||
|
};
|
||||||
|
|
|
@ -7,6 +7,6 @@ import { RealTimeModule } from '@server/socket/realtime/realtime.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [RealTimeModule],
|
imports: [RealTimeModule],
|
||||||
providers: [AppConfigService, AppConfigRouter, TrpcService],
|
providers: [AppConfigService, AppConfigRouter, TrpcService],
|
||||||
exports: [AppConfigService, AppConfigRouter]
|
exports: [AppConfigService, AppConfigRouter],
|
||||||
})
|
})
|
||||||
export class AppConfigModule { }
|
export class AppConfigModule {}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import {
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
ConflictException,
|
ConflictException,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
export const PrismaErrorCode = Object.freeze({
|
export const PrismaErrorCode = Object.freeze({
|
||||||
P2000: 'P2000',
|
P2000: 'P2000',
|
||||||
P2001: 'P2001',
|
P2001: 'P2001',
|
||||||
P2002: 'P2002',
|
P2002: 'P2002',
|
||||||
|
@ -35,32 +35,37 @@ import {
|
||||||
P1002: 'P1002',
|
P1002: 'P1002',
|
||||||
P1015: 'P1015',
|
P1015: 'P1015',
|
||||||
P1017: 'P1017',
|
P1017: 'P1017',
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PrismaErrorCode = keyof typeof PrismaErrorCode;
|
export type PrismaErrorCode = keyof typeof PrismaErrorCode;
|
||||||
|
|
||||||
|
interface PrismaErrorMeta {
|
||||||
interface PrismaErrorMeta {
|
|
||||||
target?: string;
|
target?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
relationName?: string;
|
relationName?: string;
|
||||||
details?: string;
|
details?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type operationT = 'create' | 'read' | 'update' | 'delete';
|
export type operationT = 'create' | 'read' | 'update' | 'delete';
|
||||||
|
|
||||||
export type PrismaErrorHandler = (
|
export type PrismaErrorHandler = (
|
||||||
operation: operationT,
|
operation: operationT,
|
||||||
meta?: PrismaErrorMeta,
|
meta?: PrismaErrorMeta,
|
||||||
) => Error;
|
) => Error;
|
||||||
|
|
||||||
export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = {
|
export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = {
|
||||||
P2000: (_operation, meta) => new BadRequestException(
|
P2000: (_operation, meta) =>
|
||||||
`The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.`
|
new BadRequestException(
|
||||||
|
`The provided value for ${
|
||||||
|
meta?.target || 'a field'
|
||||||
|
} is too long. Please use a shorter value.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2001: (operation, meta) => new NotFoundException(
|
P2001: (operation, meta) =>
|
||||||
`The ${meta?.model || 'record'} you are trying to ${operation} could not be found.`
|
new NotFoundException(
|
||||||
|
`The ${
|
||||||
|
meta?.model || 'record'
|
||||||
|
} you are trying to ${operation} could not be found.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2002: (operation, meta) => {
|
P2002: (operation, meta) => {
|
||||||
|
@ -68,131 +73,164 @@ import {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'create':
|
case 'create':
|
||||||
return new ConflictException(
|
return new ConflictException(
|
||||||
`A record with the same ${field} already exists. Please use a different value.`
|
`A record with the same ${field} already exists. Please use a different value.`,
|
||||||
);
|
);
|
||||||
case 'update':
|
case 'update':
|
||||||
return new ConflictException(
|
return new ConflictException(
|
||||||
`The new value for ${field} conflicts with an existing record.`
|
`The new value for ${field} conflicts with an existing record.`,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return new ConflictException(
|
return new ConflictException(
|
||||||
`Unique constraint violation on ${field}.`
|
`Unique constraint violation on ${field}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
P2003: (operation) => new BadRequestException(
|
P2003: (operation) =>
|
||||||
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`
|
new BadRequestException(
|
||||||
|
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2006: (_operation, meta) => new BadRequestException(
|
P2006: (_operation, meta) =>
|
||||||
`The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.`
|
new BadRequestException(
|
||||||
|
`The provided value for ${
|
||||||
|
meta?.target || 'a field'
|
||||||
|
} is invalid. Please correct it.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2007: (operation) => new InternalServerErrorException(
|
P2007: (operation) =>
|
||||||
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.`
|
new InternalServerErrorException(
|
||||||
|
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2008: (operation) => new InternalServerErrorException(
|
P2008: (operation) =>
|
||||||
`Failed to query the database during ${operation}. Please try again later.`
|
new InternalServerErrorException(
|
||||||
|
`Failed to query the database during ${operation}. Please try again later.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2009: (operation) => new InternalServerErrorException(
|
P2009: (operation) =>
|
||||||
`Invalid data fetched during ${operation}. Check query structure.`
|
new InternalServerErrorException(
|
||||||
|
`Invalid data fetched during ${operation}. Check query structure.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2010: () => new InternalServerErrorException(
|
P2010: () =>
|
||||||
`Invalid raw query. Ensure your query is correct and try again.`
|
new InternalServerErrorException(
|
||||||
|
`Invalid raw query. Ensure your query is correct and try again.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2011: (_operation, meta) => new BadRequestException(
|
P2011: (_operation, meta) =>
|
||||||
`The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.`
|
new BadRequestException(
|
||||||
|
`The required field ${
|
||||||
|
meta?.target || 'a field'
|
||||||
|
} is missing. Please provide it to continue.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2012: (operation, meta) => new BadRequestException(
|
P2012: (operation, meta) =>
|
||||||
`Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${operation}.`
|
new BadRequestException(
|
||||||
|
`Missing required relation ${
|
||||||
|
meta?.relationName || ''
|
||||||
|
}. Ensure all related data exists before ${operation}.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2014: (operation) => {
|
P2014: (operation) => {
|
||||||
switch (operation) {
|
switch (operation) {
|
||||||
case 'create':
|
case 'create':
|
||||||
return new BadRequestException(
|
return new BadRequestException(
|
||||||
`Cannot create record because the referenced data does not exist. Ensure related data exists.`
|
`Cannot create record because the referenced data does not exist. Ensure related data exists.`,
|
||||||
);
|
);
|
||||||
case 'delete':
|
case 'delete':
|
||||||
return new BadRequestException(
|
return new BadRequestException(
|
||||||
`Unable to delete record because it is linked to other data. Update or delete dependent records first.`
|
`Unable to delete record because it is linked to other data. Update or delete dependent records first.`,
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return new BadRequestException(`Foreign key constraint error.`);
|
return new BadRequestException(`Foreign key constraint error.`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
P2015: () => new InternalServerErrorException(
|
P2015: () =>
|
||||||
`A record with the required ID was expected but not found. Please retry.`
|
new InternalServerErrorException(
|
||||||
|
`A record with the required ID was expected but not found. Please retry.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2016: (operation) => new InternalServerErrorException(
|
P2016: (operation) =>
|
||||||
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`
|
new InternalServerErrorException(
|
||||||
|
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2017: (operation) => new InternalServerErrorException(
|
P2017: (operation) =>
|
||||||
`Connected records were not found for ${operation}. Check related data.`
|
new InternalServerErrorException(
|
||||||
|
`Connected records were not found for ${operation}. Check related data.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2018: () => new InternalServerErrorException(
|
P2018: () =>
|
||||||
`The required connection could not be established. Please check relationships.`
|
new InternalServerErrorException(
|
||||||
|
`The required connection could not be established. Please check relationships.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2019: (_operation, meta) => new InternalServerErrorException(
|
P2019: (_operation, meta) =>
|
||||||
`Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.`
|
new InternalServerErrorException(
|
||||||
|
`Invalid input for ${
|
||||||
|
meta?.details || 'a field'
|
||||||
|
}. Please ensure data conforms to expectations.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2021: (_operation, meta) => new InternalServerErrorException(
|
P2021: (_operation, meta) =>
|
||||||
`The ${meta?.model || 'model'} was not found in the database.`
|
new InternalServerErrorException(
|
||||||
|
`The ${meta?.model || 'model'} was not found in the database.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2025: (operation, meta) => new NotFoundException(
|
P2025: (operation, meta) =>
|
||||||
`The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.`
|
new NotFoundException(
|
||||||
|
`The ${
|
||||||
|
meta?.model || 'record'
|
||||||
|
} you are trying to ${operation} does not exist. It may have been deleted.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2031: () => new InternalServerErrorException(
|
P2031: () =>
|
||||||
`Invalid Prisma Client initialization error. Please check configuration.`
|
new InternalServerErrorException(
|
||||||
|
`Invalid Prisma Client initialization error. Please check configuration.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2033: (operation) => new InternalServerErrorException(
|
P2033: (operation) =>
|
||||||
`Insufficient database write permissions for ${operation}.`
|
new InternalServerErrorException(
|
||||||
|
`Insufficient database write permissions for ${operation}.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2034: (operation) => new InternalServerErrorException(
|
P2034: (operation) =>
|
||||||
`Database read-only transaction failed during ${operation}.`
|
new InternalServerErrorException(
|
||||||
|
`Database read-only transaction failed during ${operation}.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P2037: (operation) => new InternalServerErrorException(
|
P2037: (operation) =>
|
||||||
`Unsupported combinations of input types for ${operation}. Please correct the query or input.`
|
new InternalServerErrorException(
|
||||||
|
`Unsupported combinations of input types for ${operation}. Please correct the query or input.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P1000: () => new InternalServerErrorException(
|
P1000: () =>
|
||||||
`Database authentication failed. Verify your credentials and try again.`
|
new InternalServerErrorException(
|
||||||
|
`Database authentication failed. Verify your credentials and try again.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P1001: () => new InternalServerErrorException(
|
P1001: () =>
|
||||||
`The database server could not be reached. Please check its availability.`
|
new InternalServerErrorException(
|
||||||
|
`The database server could not be reached. Please check its availability.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P1002: () => new InternalServerErrorException(
|
P1002: () =>
|
||||||
`Connection to the database timed out. Verify network connectivity and server availability.`
|
new InternalServerErrorException(
|
||||||
|
`Connection to the database timed out. Verify network connectivity and server availability.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P1015: (operation) => new InternalServerErrorException(
|
P1015: (operation) =>
|
||||||
`Migration failed. Unable to complete ${operation}. Check migration history or database state.`
|
new InternalServerErrorException(
|
||||||
|
`Migration failed. Unable to complete ${operation}. Check migration history or database state.`,
|
||||||
),
|
),
|
||||||
|
|
||||||
P1017: () => new InternalServerErrorException(
|
P1017: () =>
|
||||||
`Database connection failed. Ensure the database is online and credentials are correct.`
|
new InternalServerErrorException(
|
||||||
|
`Database connection failed. Ensure the database is online and credentials are correct.`,
|
||||||
),
|
),
|
||||||
P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error {
|
P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error {
|
||||||
throw new Error('Function not implemented.');
|
throw new Error('Function not implemented.');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
import { UserProfile, RowModelRequest, RowRequestSchema } from "@nice/common";
|
import { UserProfile, RowModelRequest, RowRequestSchema } from '@nice/common';
|
||||||
import { RowModelService } from "./row-model.service";
|
import { RowModelService } from './row-model.service';
|
||||||
import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder";
|
import { isFieldCondition, LogicalCondition, SQLBuilder } from './sql-builder';
|
||||||
import EventBus from "@server/utils/event-bus";
|
import EventBus from '@server/utils/event-bus';
|
||||||
import supejson from "superjson-cjs"
|
import supejson from 'superjson-cjs';
|
||||||
import { deleteByPattern } from "@server/utils/redis/utils";
|
import { deleteByPattern } from '@server/utils/redis/utils';
|
||||||
import { redis } from "@server/utils/redis/redis.service";
|
import { redis } from '@server/utils/redis/redis.service';
|
||||||
import { z } from "zod";
|
import { z } from 'zod';
|
||||||
export class RowCacheService extends RowModelService {
|
export class RowCacheService extends RowModelService {
|
||||||
constructor(tableName: string, private enableCache: boolean = true) {
|
constructor(
|
||||||
super(tableName)
|
tableName: string,
|
||||||
|
private enableCache: boolean = true,
|
||||||
|
) {
|
||||||
|
super(tableName);
|
||||||
if (this.enableCache) {
|
if (this.enableCache) {
|
||||||
EventBus.on("dataChanged", async ({ type, data }) => {
|
EventBus.on('dataChanged', async ({ type, data }) => {
|
||||||
if (type === tableName) {
|
if (type === tableName) {
|
||||||
const dataArray = Array.isArray(data) ? data : [data];
|
const dataArray = Array.isArray(data) ? data : [data];
|
||||||
for (const item of dataArray) {
|
for (const item of dataArray) {
|
||||||
try {
|
try {
|
||||||
if (item.id) {
|
if (item.id) {
|
||||||
this.invalidateRowCacheById(item.id)
|
this.invalidateRowCacheById(item.id);
|
||||||
}
|
}
|
||||||
if (item.parentId) {
|
if (item.parentId) {
|
||||||
this.invalidateRowCacheById(item.parentId)
|
this.invalidateRowCacheById(item.parentId);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error deleting cache for type ${tableName}:`, err);
|
console.error(`Error deleting cache for type ${tableName}:`, err);
|
||||||
|
@ -38,21 +41,15 @@ export class RowCacheService extends RowModelService {
|
||||||
await deleteByPattern(pattern);
|
await deleteByPattern(pattern);
|
||||||
}
|
}
|
||||||
createJoinSql(request?: RowModelRequest): string[] {
|
createJoinSql(request?: RowModelRequest): string[] {
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
protected async getRowRelation(args: { data: any, staff?: UserProfile }) {
|
protected async getRowRelation(args: { data: any; staff?: UserProfile }) {
|
||||||
return args.data;
|
return args.data;
|
||||||
}
|
}
|
||||||
protected async setResPermissions(
|
protected async setResPermissions(data: any, staff?: UserProfile) {
|
||||||
data: any,
|
return data;
|
||||||
staff?: UserProfile,
|
|
||||||
) {
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
protected async getRowDto(
|
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
|
||||||
data: any,
|
|
||||||
staff?: UserProfile,
|
|
||||||
): Promise<any> {
|
|
||||||
// 如果没有id,直接返回原数据
|
// 如果没有id,直接返回原数据
|
||||||
if (!data?.id) return data;
|
if (!data?.id) return data;
|
||||||
// 如果未启用缓存,直接处理并返回数据
|
// 如果未启用缓存,直接处理并返回数据
|
||||||
|
@ -77,25 +74,21 @@ export class RowCacheService extends RowModelService {
|
||||||
|
|
||||||
private async getCachedData(
|
private async getCachedData(
|
||||||
key: string,
|
key: string,
|
||||||
staff?: UserProfile
|
staff?: UserProfile,
|
||||||
): Promise<any | null> {
|
): Promise<any | null> {
|
||||||
const cachedDataStr = await redis.get(key);
|
const cachedDataStr = await redis.get(key);
|
||||||
if (!cachedDataStr) return null;
|
if (!cachedDataStr) return null;
|
||||||
const cachedData = supejson.parse(cachedDataStr) as any;
|
const cachedData = supejson.parse(cachedDataStr) as any;
|
||||||
if (!cachedData?.id) return null;
|
if (!cachedData?.id) return null;
|
||||||
return staff
|
return staff ? this.setResPermissions(cachedData, staff) : cachedData;
|
||||||
? this.setResPermissions(cachedData, staff)
|
|
||||||
: cachedData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processDataWithPermissions(
|
private async processDataWithPermissions(
|
||||||
data: any,
|
data: any,
|
||||||
staff?: UserProfile
|
staff?: UserProfile,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
// 处理权限
|
// 处理权限
|
||||||
const permData = staff
|
const permData = staff ? await this.setResPermissions(data, staff) : data;
|
||||||
? await this.setResPermissions(data, staff)
|
|
||||||
: data;
|
|
||||||
// 获取关联数据
|
// 获取关联数据
|
||||||
return this.getRowRelation({ data: permData, staff });
|
return this.getRowRelation({ data: permData, staff });
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ export abstract class RowModelService {
|
||||||
// 添加更多需要引号的关键词
|
// 添加更多需要引号的关键词
|
||||||
]);
|
]);
|
||||||
protected logger = new Logger(this.tableName);
|
protected logger = new Logger(this.tableName);
|
||||||
protected constructor(protected tableName: string) { }
|
protected constructor(protected tableName: string) {}
|
||||||
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
|
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,10 @@ export abstract class RowModelService {
|
||||||
const { rowGroupCols, valueCols, groupKeys } = request;
|
const { rowGroupCols, valueCols, groupKeys } = request;
|
||||||
return valueCols.map(
|
return valueCols.map(
|
||||||
(valueCol) =>
|
(valueCol) =>
|
||||||
`${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}`,
|
`${valueCol.aggFunc}(${valueCol.field.replace(
|
||||||
|
'.',
|
||||||
|
'_',
|
||||||
|
)}) AS ${valueCol.field.split('.').join('_')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
protected createGroupingRowSelect(
|
protected createGroupingRowSelect(
|
||||||
|
@ -179,7 +182,9 @@ export abstract class RowModelService {
|
||||||
colsToSelect.push(
|
colsToSelect.push(
|
||||||
...valueCols.map(
|
...valueCols.map(
|
||||||
(valueCol) =>
|
(valueCol) =>
|
||||||
`${wrapperSql ? '' : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`,
|
`${wrapperSql ? '' : valueCol.aggFunc}(${
|
||||||
|
valueCol.field
|
||||||
|
}) AS ${valueCol.field.replace('.', '_')}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -286,7 +291,10 @@ export abstract class RowModelService {
|
||||||
protected buildAggSelect(valueCols: any[]): string[] {
|
protected buildAggSelect(valueCols: any[]): string[] {
|
||||||
return valueCols.map(
|
return valueCols.map(
|
||||||
(valueCol) =>
|
(valueCol) =>
|
||||||
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`,
|
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace(
|
||||||
|
'.',
|
||||||
|
'_',
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,38 @@
|
||||||
export interface FieldCondition {
|
export interface FieldCondition {
|
||||||
field: string;
|
field: string;
|
||||||
op: OperatorType
|
op: OperatorType;
|
||||||
type?: "text" | "number" | "date";
|
type?: 'text' | 'number' | 'date';
|
||||||
value?: any;
|
value?: any;
|
||||||
valueTo?: any;
|
valueTo?: any;
|
||||||
};
|
}
|
||||||
export type OperatorType = 'equals' | 'notEqual' | 'contains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank' | 'greaterThan' | 'lessThanOrEqual' | 'inRange' | 'lessThan' | 'greaterThan' | 'in';
|
export type OperatorType =
|
||||||
export type LogicalCondition = FieldCondition | {
|
| 'equals'
|
||||||
|
| 'notEqual'
|
||||||
|
| 'contains'
|
||||||
|
| 'startsWith'
|
||||||
|
| 'endsWith'
|
||||||
|
| 'blank'
|
||||||
|
| 'notBlank'
|
||||||
|
| 'greaterThan'
|
||||||
|
| 'lessThanOrEqual'
|
||||||
|
| 'inRange'
|
||||||
|
| 'lessThan'
|
||||||
|
| 'greaterThan'
|
||||||
|
| 'in';
|
||||||
|
export type LogicalCondition =
|
||||||
|
| FieldCondition
|
||||||
|
| {
|
||||||
AND?: LogicalCondition[];
|
AND?: LogicalCondition[];
|
||||||
OR?: LogicalCondition[];
|
OR?: LogicalCondition[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isFieldCondition(condition: LogicalCondition): condition is FieldCondition {
|
export function isFieldCondition(
|
||||||
|
condition: LogicalCondition,
|
||||||
|
): condition is FieldCondition {
|
||||||
return (condition as FieldCondition).field !== undefined;
|
return (condition as FieldCondition).field !== undefined;
|
||||||
}
|
}
|
||||||
function buildCondition(condition: FieldCondition): string {
|
function buildCondition(condition: FieldCondition): string {
|
||||||
const { field, op, value, type = "text", valueTo } = condition;
|
const { field, op, value, type = 'text', valueTo } = condition;
|
||||||
switch (op) {
|
switch (op) {
|
||||||
case 'equals':
|
case 'equals':
|
||||||
return `${field} = '${value}'`;
|
return `${field} = '${value}'`;
|
||||||
|
@ -28,15 +45,11 @@ function buildCondition(condition: FieldCondition): string {
|
||||||
case 'endsWith':
|
case 'endsWith':
|
||||||
return `${field} LIKE '%${value}'`;
|
return `${field} LIKE '%${value}'`;
|
||||||
case 'blank':
|
case 'blank':
|
||||||
if (type !== "date")
|
if (type !== 'date') return `(${field} IS NULL OR ${field} = '')`;
|
||||||
return `(${field} IS NULL OR ${field} = '')`;
|
else return `${field} IS NULL`;
|
||||||
else
|
|
||||||
return `${field} IS NULL`;
|
|
||||||
case 'notBlank':
|
case 'notBlank':
|
||||||
if (type !== 'date')
|
if (type !== 'date') return `${field} IS NOT NULL AND ${field} != ''`;
|
||||||
return `${field} IS NOT NULL AND ${field} != ''`;
|
else return `${field} IS NOT NULL`;
|
||||||
else
|
|
||||||
return `${field} IS NOT NULL`;
|
|
||||||
case 'greaterThan':
|
case 'greaterThan':
|
||||||
return `${field} > '${value}'`;
|
return `${field} > '${value}'`;
|
||||||
case 'lessThanOrEqual':
|
case 'lessThanOrEqual':
|
||||||
|
@ -52,7 +65,9 @@ function buildCondition(condition: FieldCondition): string {
|
||||||
// Return a condition that is always false if value is empty or an empty array
|
// Return a condition that is always false if value is empty or an empty array
|
||||||
return '1 = 0';
|
return '1 = 0';
|
||||||
}
|
}
|
||||||
return `${field} IN (${(value as any[]).map(val => `'${val}'`).join(', ')})`;
|
return `${field} IN (${(value as any[])
|
||||||
|
.map((val) => `'${val}'`)
|
||||||
|
.join(', ')})`;
|
||||||
default:
|
default:
|
||||||
return 'true'; // Default return for unmatched conditions
|
return 'true'; // Default return for unmatched conditions
|
||||||
}
|
}
|
||||||
|
@ -63,18 +78,18 @@ function buildLogicalCondition(logicalCondition: LogicalCondition): string {
|
||||||
}
|
}
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (logicalCondition.AND && logicalCondition.AND.length > 0) {
|
if (logicalCondition.AND && logicalCondition.AND.length > 0) {
|
||||||
const andParts = logicalCondition.AND
|
const andParts = logicalCondition.AND.map((c) =>
|
||||||
.map(c => buildLogicalCondition(c))
|
buildLogicalCondition(c),
|
||||||
.filter(part => part !== ''); // Filter out empty conditions
|
).filter((part) => part !== ''); // Filter out empty conditions
|
||||||
if (andParts.length > 0) {
|
if (andParts.length > 0) {
|
||||||
parts.push(`(${andParts.join(' AND ')})`);
|
parts.push(`(${andParts.join(' AND ')})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Process OR conditions
|
// Process OR conditions
|
||||||
if (logicalCondition.OR && logicalCondition.OR.length > 0) {
|
if (logicalCondition.OR && logicalCondition.OR.length > 0) {
|
||||||
const orParts = logicalCondition.OR
|
const orParts = logicalCondition.OR.map((c) =>
|
||||||
.map(c => buildLogicalCondition(c))
|
buildLogicalCondition(c),
|
||||||
.filter(part => part !== ''); // Filter out empty conditions
|
).filter((part) => part !== ''); // Filter out empty conditions
|
||||||
if (orParts.length > 0) {
|
if (orParts.length > 0) {
|
||||||
parts.push(`(${orParts.join(' OR ')})`);
|
parts.push(`(${orParts.join(' OR ')})`);
|
||||||
}
|
}
|
||||||
|
@ -85,12 +100,18 @@ function buildLogicalCondition(logicalCondition: LogicalCondition): string {
|
||||||
|
|
||||||
export class SQLBuilder {
|
export class SQLBuilder {
|
||||||
static select(fields: string[], distinctField?: string): string {
|
static select(fields: string[], distinctField?: string): string {
|
||||||
const distinctClause = distinctField ? `DISTINCT ON (${distinctField}) ` : "";
|
const distinctClause = distinctField
|
||||||
return `SELECT ${distinctClause}${fields.join(", ")}`;
|
? `DISTINCT ON (${distinctField}) `
|
||||||
|
: '';
|
||||||
|
return `SELECT ${distinctClause}${fields.join(', ')}`;
|
||||||
}
|
}
|
||||||
static rowNumber(orderBy: string, partitionBy: string | null = null, alias: string = 'row_num'): string {
|
static rowNumber(
|
||||||
|
orderBy: string,
|
||||||
|
partitionBy: string | null = null,
|
||||||
|
alias: string = 'row_num',
|
||||||
|
): string {
|
||||||
if (!orderBy) {
|
if (!orderBy) {
|
||||||
throw new Error("orderBy 参数不能为空");
|
throw new Error('orderBy 参数不能为空');
|
||||||
}
|
}
|
||||||
|
|
||||||
let partitionClause = '';
|
let partitionClause = '';
|
||||||
|
@ -106,15 +127,15 @@ export class SQLBuilder {
|
||||||
|
|
||||||
static where(conditions: LogicalCondition): string {
|
static where(conditions: LogicalCondition): string {
|
||||||
const whereClause = buildLogicalCondition(conditions);
|
const whereClause = buildLogicalCondition(conditions);
|
||||||
return whereClause ? `WHERE ${whereClause}` : "";
|
return whereClause ? `WHERE ${whereClause}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static groupBy(columns: string[]): string {
|
static groupBy(columns: string[]): string {
|
||||||
return columns.length ? `GROUP BY ${columns.join(", ")}` : "";
|
return columns.length ? `GROUP BY ${columns.join(', ')}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static orderBy(columns: string[]): string {
|
static orderBy(columns: string[]): string {
|
||||||
return columns.length ? `ORDER BY ${columns.join(", ")}` : "";
|
return columns.length ? `ORDER BY ${columns.join(', ')}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
static limit(pageSize: number, offset: number = 0): string {
|
static limit(pageSize: number, offset: number = 0): string {
|
||||||
|
@ -125,14 +146,27 @@ export class SQLBuilder {
|
||||||
return clauses.filter(Boolean).join(' ');
|
return clauses.filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
static createFilterSql(key: string, item: any): LogicalCondition {
|
static createFilterSql(key: string, item: any): LogicalCondition {
|
||||||
const conditionFuncs: Record<string, (item: { values?: any[], dateFrom?: string, dateTo?: string, filter: any, type: OperatorType, filterType: OperatorType }) => LogicalCondition> = {
|
const conditionFuncs: Record<
|
||||||
|
string,
|
||||||
|
(item: {
|
||||||
|
values?: any[];
|
||||||
|
dateFrom?: string;
|
||||||
|
dateTo?: string;
|
||||||
|
filter: any;
|
||||||
|
type: OperatorType;
|
||||||
|
filterType: OperatorType;
|
||||||
|
}) => LogicalCondition
|
||||||
|
> = {
|
||||||
text: (item) => ({ value: item.filter, op: item.type, field: key }),
|
text: (item) => ({ value: item.filter, op: item.type, field: key }),
|
||||||
number: (item) => ({ value: item.filter, op: item.type, field: key }),
|
number: (item) => ({ value: item.filter, op: item.type, field: key }),
|
||||||
date: (item) => ({ value: item.dateFrom, valueTo: item.dateTo, op: item.type, field: key }),
|
date: (item) => ({
|
||||||
set: (item) => ({ value: item.values, op: "in", field: key })
|
value: item.dateFrom,
|
||||||
}
|
valueTo: item.dateTo,
|
||||||
return conditionFuncs[item.filterType](item)
|
op: item.type,
|
||||||
|
field: key,
|
||||||
|
}),
|
||||||
|
set: (item) => ({ value: item.values, op: 'in', field: key }),
|
||||||
|
};
|
||||||
|
return conditionFuncs[item.filterType](item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Controller, UseGuards } from "@nestjs/common";
|
import { Controller, UseGuards } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@server/auth/auth.guard';
|
import { AuthGuard } from '@server/auth/auth.guard';
|
||||||
import { DailyTrainService } from "./dailyTrain.service";
|
import { DailyTrainService } from './dailyTrain.service';
|
||||||
|
|
||||||
@Controller('train-content')
|
@Controller('train-content')
|
||||||
export class DailyTrainController {
|
export class DailyTrainController {
|
||||||
|
|
|
@ -5,11 +5,10 @@ import { DailyTrainController } from './dailyTrain.controller';
|
||||||
import { DailyTrainService } from './dailyTrain.service';
|
import { DailyTrainService } from './dailyTrain.service';
|
||||||
import { DailyTrainRouter } from './dailyTrain.router';
|
import { DailyTrainRouter } from './dailyTrain.router';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StaffModule],
|
imports: [StaffModule],
|
||||||
controllers: [DailyTrainController],
|
controllers: [DailyTrainController],
|
||||||
providers: [DailyTrainService,DailyTrainRouter,TrpcService],
|
providers: [DailyTrainService, DailyTrainRouter, TrpcService],
|
||||||
exports: [DailyTrainService,DailyTrainRouter],
|
exports: [DailyTrainService, DailyTrainRouter],
|
||||||
})
|
})
|
||||||
export class DailyTrainModule {}
|
export class DailyTrainModule {}
|
|
@ -1,16 +1,13 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrpcService } from "@server/trpc/trpc.service";
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { DailyTrainService } from "./dailyTrain.service";
|
import { DailyTrainService } from './dailyTrain.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DailyTrainRouter {
|
export class DailyTrainRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly dailyTrainService: DailyTrainService,
|
private readonly dailyTrainService: DailyTrainService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
router = this.trpc.router({
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
router = this.trpc.router({});
|
||||||
}
|
}
|
|
@ -1,38 +1,35 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { BaseService } from "../base/base.service";
|
import { BaseService } from '../base/base.service';
|
||||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
|
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db,ObjectType.DAILY_TRAIN,true);
|
super(db, ObjectType.DAILY_TRAIN, true);
|
||||||
}
|
}
|
||||||
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
||||||
console.log(args)
|
console.log(args);
|
||||||
const result = await super.create(args)
|
const result = await super.create(args);
|
||||||
this.emitDataChanged(CrudOperation.CREATED,result)
|
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(args:Prisma.DailyTrainTimeUpdateArgs){
|
async update(args: Prisma.DailyTrainTimeUpdateArgs) {
|
||||||
const result = await super.update(args)
|
const result = await super.update(args);
|
||||||
this.emitDataChanged(CrudOperation.UPDATED,result)
|
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
||||||
const result = await super.findMany(args);
|
const result = await super.findMany(args);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||||
EventBus.emit('dataChanged', {
|
EventBus.emit('dataChanged', {
|
||||||
type:ObjectType.DAILY_TRAIN,
|
type: ObjectType.DAILY_TRAIN,
|
||||||
operation,
|
operation,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { db } from '@nice/common';
|
||||||
|
|
||||||
@Controller('dept')
|
@Controller('dept')
|
||||||
export class DepartmentController {
|
export class DepartmentController {
|
||||||
constructor(private readonly deptService: DepartmentService) { }
|
constructor(private readonly deptService: DepartmentService) {}
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@Get('get-detail')
|
@Get('get-detail')
|
||||||
async getDepartmentDetails(@Query('dept-id') deptId: string) {
|
async getDepartmentDetails(@Query('dept-id') deptId: string) {
|
||||||
|
|
|
@ -6,8 +6,13 @@ import { DepartmentController } from './department.controller';
|
||||||
import { DepartmentRowService } from './department.row.service';
|
import { DepartmentRowService } from './department.row.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [DepartmentService, DepartmentRouter, DepartmentRowService, TrpcService],
|
providers: [
|
||||||
|
DepartmentService,
|
||||||
|
DepartmentRouter,
|
||||||
|
DepartmentRowService,
|
||||||
|
TrpcService,
|
||||||
|
],
|
||||||
exports: [DepartmentService, DepartmentRouter],
|
exports: [DepartmentService, DepartmentRouter],
|
||||||
controllers: [DepartmentController],
|
controllers: [DepartmentController],
|
||||||
})
|
})
|
||||||
export class DepartmentModule { }
|
export class DepartmentModule {}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { db, VisitType } from '@nice/common';
|
||||||
|
|
||||||
@Controller('message')
|
@Controller('message')
|
||||||
export class MessageController {
|
export class MessageController {
|
||||||
constructor(private readonly messageService: MessageService) { }
|
constructor(private readonly messageService: MessageService) {}
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
@Get('find-last-one')
|
@Get('find-last-one')
|
||||||
async findLastOne(@Query('staff-id') staffId: string) {
|
async findLastOne(@Query('staff-id') staffId: string) {
|
||||||
|
@ -27,7 +27,7 @@ export class MessageController {
|
||||||
select: {
|
select: {
|
||||||
title: true,
|
title: true,
|
||||||
content: true,
|
content: true,
|
||||||
url: true
|
url: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export class MessageController {
|
||||||
visits: {
|
visits: {
|
||||||
none: {
|
none: {
|
||||||
id: staffId,
|
id: staffId,
|
||||||
type: VisitType.READED
|
type: VisitType.READED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
receivers: {
|
receivers: {
|
||||||
|
@ -92,7 +92,7 @@ export class MessageController {
|
||||||
visits: {
|
visits: {
|
||||||
none: {
|
none: {
|
||||||
id: staffId,
|
id: staffId,
|
||||||
type: VisitType.READED
|
type: VisitType.READED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
receivers: {
|
receivers: {
|
||||||
|
|
|
@ -11,4 +11,4 @@ import { MessageController } from './message.controller';
|
||||||
exports: [MessageService, MessageRouter],
|
exports: [MessageService, MessageRouter],
|
||||||
controllers: [MessageController],
|
controllers: [MessageController],
|
||||||
})
|
})
|
||||||
export class MessageModule { }
|
export class MessageModule {}
|
||||||
|
|
|
@ -3,15 +3,16 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { MessageService } from './message.service';
|
import { MessageService } from './message.service';
|
||||||
import { Prisma } from '@nice/common';
|
import { Prisma } from '@nice/common';
|
||||||
import { z, ZodType } from 'zod';
|
import { z, ZodType } from 'zod';
|
||||||
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> = z.any()
|
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> =
|
||||||
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any()
|
z.any();
|
||||||
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any()
|
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any();
|
||||||
|
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any();
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MessageRouter {
|
export class MessageRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly messageService: MessageService,
|
private readonly messageService: MessageService,
|
||||||
) { }
|
) {}
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.procedure
|
create: this.trpc.procedure
|
||||||
.input(MessageUncheckedCreateInputSchema)
|
.input(MessageUncheckedCreateInputSchema)
|
||||||
|
@ -20,20 +21,21 @@ export class MessageRouter {
|
||||||
return await this.messageService.create({ data: input }, { staff });
|
return await this.messageService.create({ data: input }, { staff });
|
||||||
}),
|
}),
|
||||||
findManyWithCursor: this.trpc.protectProcedure
|
findManyWithCursor: this.trpc.protectProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
cursor: z.any().nullish(),
|
cursor: z.any().nullish(),
|
||||||
take: z.number().nullish(),
|
take: z.number().nullish(),
|
||||||
where: MessageWhereInputSchema.nullish(),
|
where: MessageWhereInputSchema.nullish(),
|
||||||
select: MessageSelectSchema.nullish()
|
select: MessageSelectSchema.nullish(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.messageService.findManyWithCursor(input, staff);
|
return await this.messageService.findManyWithCursor(input, staff);
|
||||||
}),
|
}),
|
||||||
getUnreadCount: this.trpc.protectProcedure
|
getUnreadCount: this.trpc.protectProcedure.query(async ({ ctx }) => {
|
||||||
.query(async ({ ctx }) => {
|
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.messageService.getUnreadCount(staff);
|
return await this.messageService.getUnreadCount(staff);
|
||||||
})
|
}),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,26 +8,28 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db, ObjectType.MESSAGE);
|
super(db, ObjectType.MESSAGE);
|
||||||
}
|
}
|
||||||
async create(args: Prisma.MessageCreateArgs, params?: { tx?: Prisma.MessageDelegate, staff?: UserProfile }) {
|
async create(
|
||||||
|
args: Prisma.MessageCreateArgs,
|
||||||
|
params?: { tx?: Prisma.MessageDelegate; staff?: UserProfile },
|
||||||
|
) {
|
||||||
args.data!.senderId = params?.staff?.id;
|
args.data!.senderId = params?.staff?.id;
|
||||||
args.include = {
|
args.include = {
|
||||||
receivers: {
|
receivers: {
|
||||||
select: { id: true, registerToken: true, username: true }
|
select: { id: true, registerToken: true, username: true },
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
const result = await super.create(args);
|
const result = await super.create(args);
|
||||||
EventBus.emit("dataChanged", {
|
EventBus.emit('dataChanged', {
|
||||||
type: ObjectType.MESSAGE,
|
type: ObjectType.MESSAGE,
|
||||||
operation: CrudOperation.CREATED,
|
operation: CrudOperation.CREATED,
|
||||||
data: result
|
data: result,
|
||||||
})
|
});
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
async findManyWithCursor(
|
async findManyWithCursor(
|
||||||
args: Prisma.MessageFindManyArgs,
|
args: Prisma.MessageFindManyArgs,
|
||||||
staff?: UserProfile,
|
staff?: UserProfile,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
||||||
let { items } = result;
|
let { items } = result;
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -46,12 +48,12 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
|
||||||
visits: {
|
visits: {
|
||||||
none: {
|
none: {
|
||||||
visitorId: staff?.id,
|
visitorId: staff?.id,
|
||||||
type: VisitType.READED
|
type: VisitType.READED,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
return count
|
return count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { Message, UserProfile, VisitType, db } from "@nice/common"
|
import { Message, UserProfile, VisitType, db } from '@nice/common';
|
||||||
export async function setMessageRelation(
|
export async function setMessageRelation(
|
||||||
data: Message,
|
data: Message,
|
||||||
staff?: UserProfile,
|
staff?: UserProfile,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
|
||||||
const readed =
|
const readed =
|
||||||
(await db.visit.count({
|
(await db.visit.count({
|
||||||
where: {
|
where: {
|
||||||
|
@ -13,8 +12,7 @@ export async function setMessageRelation(
|
||||||
},
|
},
|
||||||
})) > 0;
|
})) > 0;
|
||||||
|
|
||||||
|
|
||||||
Object.assign(data, {
|
Object.assign(data, {
|
||||||
readed
|
readed,
|
||||||
})
|
});
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Controller, UseGuards } from "@nestjs/common";
|
import { Controller, UseGuards } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@server/auth/auth.guard';
|
import { AuthGuard } from '@server/auth/auth.guard';
|
||||||
import { DailyTrainService } from "./dailyTrain.service";
|
import { DailyTrainService } from './dailyTrain.service';
|
||||||
|
|
||||||
@Controller('train-content')
|
@Controller('train-content')
|
||||||
export class DailyTrainController {
|
export class DailyTrainController {
|
||||||
|
|
|
@ -5,11 +5,10 @@ import { DailyTrainController } from './dailyTrain.controller';
|
||||||
import { DailyTrainService } from './dailyTrain.service';
|
import { DailyTrainService } from './dailyTrain.service';
|
||||||
import { DailyTrainRouter } from './dailyTrain.router';
|
import { DailyTrainRouter } from './dailyTrain.router';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StaffModule],
|
imports: [StaffModule],
|
||||||
controllers: [DailyTrainController],
|
controllers: [DailyTrainController],
|
||||||
providers: [DailyTrainService,DailyTrainRouter,TrpcService],
|
providers: [DailyTrainService, DailyTrainRouter, TrpcService],
|
||||||
exports: [DailyTrainService,DailyTrainRouter],
|
exports: [DailyTrainService, DailyTrainRouter],
|
||||||
})
|
})
|
||||||
export class DailyTrainModule {}
|
export class DailyTrainModule {}
|
|
@ -1,16 +1,13 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrpcService } from "@server/trpc/trpc.service";
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { DailyTrainService } from "./dailyTrain.service";
|
import { DailyTrainService } from './dailyTrain.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DailyTrainRouter {
|
export class DailyTrainRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly dailyTrainService: DailyTrainService,
|
private readonly dailyTrainService: DailyTrainService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
router = this.trpc.router({
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
router = this.trpc.router({});
|
||||||
}
|
}
|
|
@ -1,38 +1,35 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { BaseService } from "../base/base.service";
|
import { BaseService } from '../base/base.service';
|
||||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
|
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db,ObjectType.DAILY_TRAIN,true);
|
super(db, ObjectType.DAILY_TRAIN, true);
|
||||||
}
|
}
|
||||||
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
||||||
console.log(args)
|
console.log(args);
|
||||||
const result = await super.create(args)
|
const result = await super.create(args);
|
||||||
this.emitDataChanged(CrudOperation.CREATED,result)
|
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(args:Prisma.DailyTrainTimeUpdateArgs){
|
async update(args: Prisma.DailyTrainTimeUpdateArgs) {
|
||||||
const result = await super.update(args)
|
const result = await super.update(args);
|
||||||
this.emitDataChanged(CrudOperation.UPDATED,result)
|
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
||||||
const result = await super.findMany(args);
|
const result = await super.findMany(args);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||||
EventBus.emit('dataChanged', {
|
EventBus.emit('dataChanged', {
|
||||||
type:ObjectType.DAILY_TRAIN,
|
type: ObjectType.DAILY_TRAIN,
|
||||||
operation,
|
operation,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,13 @@ import { DepartmentModule } from '../department/department.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DepartmentModule],
|
imports: [DepartmentModule],
|
||||||
providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter],
|
providers: [
|
||||||
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter]
|
RoleMapService,
|
||||||
|
RoleRouter,
|
||||||
|
TrpcService,
|
||||||
|
RoleService,
|
||||||
|
RoleMapRouter,
|
||||||
|
],
|
||||||
|
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter],
|
||||||
})
|
})
|
||||||
export class RoleMapModule { }
|
export class RoleMapModule {}
|
||||||
|
|
|
@ -3,21 +3,21 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { Prisma, UpdateOrderSchema } from '@nice/common';
|
import { Prisma, UpdateOrderSchema } from '@nice/common';
|
||||||
import { RoleService } from './role.service';
|
import { RoleService } from './role.service';
|
||||||
import { z, ZodType } from 'zod';
|
import { z, ZodType } from 'zod';
|
||||||
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any()
|
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any();
|
||||||
const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any()
|
const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any();
|
||||||
const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any()
|
const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any();
|
||||||
const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any()
|
const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any();
|
||||||
const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any()
|
const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any();
|
||||||
const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any()
|
const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any();
|
||||||
const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any()
|
const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any();
|
||||||
const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any()
|
const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any();
|
||||||
const RoleUpdateInputSchema: ZodType<Prisma.RoleUpdateInput> = z.any();
|
const RoleUpdateInputSchema: ZodType<Prisma.RoleUpdateInput> = z.any();
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RoleRouter {
|
export class RoleRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly roleService: RoleService,
|
private readonly roleService: RoleService,
|
||||||
) { }
|
) {}
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.protectProcedure
|
create: this.trpc.protectProcedure
|
||||||
.input(RoleCreateArgsSchema)
|
.input(RoleCreateArgsSchema)
|
||||||
|
@ -31,7 +31,8 @@ export class RoleRouter {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.roleService.update(input, staff);
|
return await this.roleService.update(input, staff);
|
||||||
}),
|
}),
|
||||||
createMany: this.trpc.protectProcedure.input(z.array(RoleCreateManyInputSchema))
|
createMany: this.trpc.protectProcedure
|
||||||
|
.input(z.array(RoleCreateManyInputSchema))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ export class RoleRouter {
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
ids: z.array(z.string()),
|
ids: z.array(z.string()),
|
||||||
data: RoleUpdateInputSchema.optional()
|
data: RoleUpdateInputSchema.optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
|
@ -64,23 +65,27 @@ export class RoleRouter {
|
||||||
return await this.roleService.findMany(input);
|
return await this.roleService.findMany(input);
|
||||||
}),
|
}),
|
||||||
findManyWithCursor: this.trpc.protectProcedure
|
findManyWithCursor: this.trpc.protectProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
cursor: z.any().nullish(),
|
cursor: z.any().nullish(),
|
||||||
take: z.number().optional(),
|
take: z.number().optional(),
|
||||||
where: RoleWhereInputSchema.optional(),
|
where: RoleWhereInputSchema.optional(),
|
||||||
select: RoleSelectSchema.optional()
|
select: RoleSelectSchema.optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.roleService.findManyWithCursor(input);
|
return await this.roleService.findManyWithCursor(input);
|
||||||
}),
|
}),
|
||||||
findManyWithPagination: this.trpc.procedure
|
findManyWithPagination: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
page: z.number(),
|
page: z.number(),
|
||||||
pageSize: z.number().optional(),
|
pageSize: z.number().optional(),
|
||||||
where: RoleWhereInputSchema.optional(),
|
where: RoleWhereInputSchema.optional(),
|
||||||
select: RoleSelectSchema.optional()
|
select: RoleSelectSchema.optional(),
|
||||||
})) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
}),
|
||||||
|
) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await this.roleService.findManyWithPagination(input);
|
return await this.roleService.findManyWithPagination(input);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,45 +1,57 @@
|
||||||
import { db, ObjectType, RowModelRequest, RowRequestSchema, UserProfile } from "@nice/common";
|
import {
|
||||||
import { RowCacheService } from "../base/row-cache.service";
|
db,
|
||||||
import { isFieldCondition, LogicalCondition } from "../base/sql-builder";
|
ObjectType,
|
||||||
import { z } from "zod";
|
RowModelRequest,
|
||||||
|
RowRequestSchema,
|
||||||
|
UserProfile,
|
||||||
|
} from '@nice/common';
|
||||||
|
import { RowCacheService } from '../base/row-cache.service';
|
||||||
|
import { isFieldCondition, LogicalCondition } from '../base/sql-builder';
|
||||||
|
import { z } from 'zod';
|
||||||
export class RoleRowService extends RowCacheService {
|
export class RoleRowService extends RowCacheService {
|
||||||
protected createGetRowsFilters(
|
protected createGetRowsFilters(
|
||||||
request: z.infer<typeof RowRequestSchema>,
|
request: z.infer<typeof RowRequestSchema>,
|
||||||
staff?: UserProfile
|
staff?: UserProfile,
|
||||||
) {
|
) {
|
||||||
const condition = super.createGetRowsFilters(request)
|
const condition = super.createGetRowsFilters(request);
|
||||||
if (isFieldCondition(condition))
|
if (isFieldCondition(condition)) return {};
|
||||||
return {}
|
const baseModelCondition: LogicalCondition[] = [
|
||||||
const baseModelCondition: LogicalCondition[] = [{
|
{
|
||||||
field: `${this.tableName}.deleted_at`,
|
field: `${this.tableName}.deleted_at`,
|
||||||
op: "blank",
|
op: 'blank',
|
||||||
type: "date"
|
type: 'date',
|
||||||
}]
|
},
|
||||||
condition.AND = [...baseModelCondition, ...condition.AND!]
|
];
|
||||||
return condition
|
condition.AND = [...baseModelCondition, ...condition.AND!];
|
||||||
|
return condition;
|
||||||
}
|
}
|
||||||
createUnGroupingRowSelect(): string[] {
|
createUnGroupingRowSelect(): string[] {
|
||||||
return [
|
return [
|
||||||
`${this.tableName}.id AS id`,
|
`${this.tableName}.id AS id`,
|
||||||
`${this.tableName}.name AS name`,
|
`${this.tableName}.name AS name`,
|
||||||
`${this.tableName}.system AS system`,
|
`${this.tableName}.system AS system`,
|
||||||
`${this.tableName}.permissions AS permissions`
|
`${this.tableName}.permissions AS permissions`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
|
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
|
||||||
if (!data.id)
|
if (!data.id) return data;
|
||||||
return data
|
|
||||||
const roleMaps = await db.roleMap.findMany({
|
const roleMaps = await db.roleMap.findMany({
|
||||||
where: {
|
where: {
|
||||||
roleId: data.id
|
roleId: data.id,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
const deptIds = roleMaps.filter(item => item.objectType === ObjectType.DEPARTMENT).map(roleMap => roleMap.objectId)
|
const deptIds = roleMaps
|
||||||
const staffIds = roleMaps.filter(item => item.objectType === ObjectType.STAFF).map(roleMap => roleMap.objectId)
|
.filter((item) => item.objectType === ObjectType.DEPARTMENT)
|
||||||
const depts = await db.department.findMany({ where: { id: { in: deptIds } } })
|
.map((roleMap) => roleMap.objectId);
|
||||||
const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } })
|
const staffIds = roleMaps
|
||||||
const result = { ...data, depts, staffs }
|
.filter((item) => item.objectType === ObjectType.STAFF)
|
||||||
return result
|
.map((roleMap) => roleMap.objectId);
|
||||||
|
const depts = await db.department.findMany({
|
||||||
|
where: { id: { in: deptIds } },
|
||||||
|
});
|
||||||
|
const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } });
|
||||||
|
const result = { ...data, depts, staffs };
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
createJoinSql(request?: RowModelRequest): string[] {
|
createJoinSql(request?: RowModelRequest): string[] {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import {
|
import { ObjectType, RoleMapMethodSchema } from '@nice/common';
|
||||||
ObjectType,
|
|
||||||
RoleMapMethodSchema,
|
|
||||||
} from '@nice/common';
|
|
||||||
import { RoleMapService } from './rolemap.service';
|
import { RoleMapService } from './rolemap.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -11,7 +8,7 @@ export class RoleMapRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly roleMapService: RoleMapService,
|
private readonly roleMapService: RoleMapService,
|
||||||
) { }
|
) {}
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
deleteAllRolesForObject: this.trpc.protectProcedure
|
deleteAllRolesForObject: this.trpc.protectProcedure
|
||||||
.input(RoleMapMethodSchema.deleteWithObject)
|
.input(RoleMapMethodSchema.deleteWithObject)
|
||||||
|
|
|
@ -64,10 +64,7 @@ export class RoleMapService extends RowModelService {
|
||||||
return condition;
|
return condition;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getRowDto(
|
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
|
||||||
row: any,
|
|
||||||
staff?: UserProfile,
|
|
||||||
): Promise<any> {
|
|
||||||
if (!row.id) return row;
|
if (!row.id) return row;
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
@ -126,15 +123,17 @@ export class RoleMapService extends RowModelService {
|
||||||
data: roleMaps,
|
data: roleMaps,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const wrapResult = Promise.all(result.map(async item => {
|
const wrapResult = Promise.all(
|
||||||
|
result.map(async (item) => {
|
||||||
const staff = await db.staff.findMany({
|
const staff = await db.staff.findMany({
|
||||||
include: { department: true },
|
include: { department: true },
|
||||||
where: {
|
where: {
|
||||||
id: item.objectId
|
id: item.objectId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
return { ...item, staff }
|
return { ...item, staff };
|
||||||
}))
|
}),
|
||||||
|
);
|
||||||
return wrapResult;
|
return wrapResult;
|
||||||
}
|
}
|
||||||
async addRoleForObjects(
|
async addRoleForObjects(
|
||||||
|
@ -260,7 +259,9 @@ export class RoleMapService extends RowModelService {
|
||||||
// const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
|
// const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
|
||||||
return { items, totalCount };
|
return { items, totalCount };
|
||||||
}
|
}
|
||||||
async getStaffsNotMap(data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>) {
|
async getStaffsNotMap(
|
||||||
|
data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>,
|
||||||
|
) {
|
||||||
const { domainId, roleId } = data;
|
const { domainId, roleId } = data;
|
||||||
let staffs = await db.staff.findMany({
|
let staffs = await db.staff.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
@ -300,7 +301,9 @@ export class RoleMapService extends RowModelService {
|
||||||
* @param data 包含角色ID和域ID的数据
|
* @param data 包含角色ID和域ID的数据
|
||||||
* @returns 角色映射详情,包含部门ID和员工ID列表
|
* @returns 角色映射详情,包含部门ID和员工ID列表
|
||||||
*/
|
*/
|
||||||
async getRoleMapDetail(data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>) {
|
async getRoleMapDetail(
|
||||||
|
data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>,
|
||||||
|
) {
|
||||||
const { roleId, domainId } = data;
|
const { roleId, domainId } = data;
|
||||||
const res = await db.roleMap.findMany({ where: { roleId, domainId } });
|
const res = await db.roleMap.findMany({ where: { roleId, domainId } });
|
||||||
|
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
import path, { dirname } from "path";
|
import path, { dirname } from 'path';
|
||||||
import { FileMetadata, VideoMetadata, ResourceProcessor } from "../types";
|
import { FileMetadata, VideoMetadata, ResourceProcessor } from '../types';
|
||||||
import { Resource, ResourceStatus, db } from "@nice/common";
|
import { Resource, ResourceStatus, db } from '@nice/common';
|
||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from '@nestjs/common';
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
export abstract class BaseProcessor implements ResourceProcessor {
|
export abstract class BaseProcessor implements ResourceProcessor {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
protected logger = new Logger(BaseProcessor.name)
|
protected logger = new Logger(BaseProcessor.name);
|
||||||
|
|
||||||
abstract process(resource: Resource): Promise<Resource>
|
abstract process(resource: Resource): Promise<Resource>;
|
||||||
protected createOutputDir(filepath: string, subdirectory: string = 'assets'): string {
|
protected createOutputDir(
|
||||||
const outputDir = path.join(
|
filepath: string,
|
||||||
path.dirname(filepath),
|
subdirectory: string = 'assets',
|
||||||
subdirectory,
|
): string {
|
||||||
|
const outputDir = path.join(path.dirname(filepath), subdirectory);
|
||||||
|
fs.mkdir(outputDir, { recursive: true }).catch((err) =>
|
||||||
|
this.logger.error(`Failed to create directory: ${err.message}`),
|
||||||
);
|
);
|
||||||
fs.mkdir(outputDir, { recursive: true }).catch(err => this.logger.error(`Failed to create directory: ${err.message}`));
|
|
||||||
|
|
||||||
return outputDir;
|
return outputDir;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//
|
//
|
|
@ -7,4 +7,4 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
exports: [ResourceRouter, ResourceService],
|
exports: [ResourceRouter, ResourceService],
|
||||||
providers: [ResourceRouter, ResourceService, TrpcService],
|
providers: [ResourceRouter, ResourceService, TrpcService],
|
||||||
})
|
})
|
||||||
export class ResourceModule { }
|
export class ResourceModule {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Injectable ,Logger} from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { BaseService } from '../base/base.service';
|
import { BaseService } from '../base/base.service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -51,7 +51,9 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
where: { fileId },
|
where: { fileId },
|
||||||
data: { fileName },
|
data: { fileName },
|
||||||
});
|
});
|
||||||
this.logger.log(`更新了现有记录的文件名 "${fileName}" 到文件 ${fileId}`);
|
this.logger.log(
|
||||||
|
`更新了现有记录的文件名 "${fileName}" 到文件 ${fileId}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 如果记录不存在,创建新记录
|
// 如果记录不存在,创建新记录
|
||||||
await db.shareCode.create({
|
await db.shareCode.create({
|
||||||
|
@ -63,7 +65,9 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
isUsed: false,
|
isUsed: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.logger.log(`创建了新记录并保存文件名 "${fileName}" 到文件 ${fileId}`);
|
this.logger.log(
|
||||||
|
`创建了新记录并保存文件名 "${fileName}" 到文件 ${fileId}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`保存文件名失败,文件ID: ${fileId}`, error);
|
this.logger.error(`保存文件名失败,文件ID: ${fileId}`, error);
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import { Resource } from "@nice/common";
|
import { Resource } from '@nice/common';
|
||||||
|
|
||||||
export interface ResourceProcessor {
|
export interface ResourceProcessor {
|
||||||
process(resource: Resource): Promise<any>
|
process(resource: Resource): Promise<any>;
|
||||||
}
|
}
|
||||||
export interface ProcessResult {
|
export interface ProcessResult {
|
||||||
success: boolean
|
success: boolean;
|
||||||
resource: Resource
|
resource: Resource;
|
||||||
error?: Error
|
error?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseMetadata {
|
export interface BaseMetadata {
|
||||||
size: number
|
size: number;
|
||||||
filetype: string
|
filetype: string;
|
||||||
filename: string
|
filename: string;
|
||||||
extension: string
|
extension: string;
|
||||||
modifiedAt: Date
|
modifiedAt: Date;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 图片特有元数据接口
|
* 图片特有元数据接口
|
||||||
|
@ -37,7 +37,7 @@ export interface VideoMetadata {
|
||||||
duration?: number;
|
duration?: number;
|
||||||
videoCodec?: string;
|
videoCodec?: string;
|
||||||
audioCodec?: string;
|
audioCodec?: string;
|
||||||
coverUrl?: string
|
coverUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,5 +51,7 @@ export interface AudioMetadata {
|
||||||
codec?: string; // 音频编码格式
|
codec?: string; // 音频编码格式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FileMetadata = ImageMetadata &
|
||||||
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata
|
VideoMetadata &
|
||||||
|
AudioMetadata &
|
||||||
|
BaseMetadata;
|
||||||
|
|
|
@ -11,6 +11,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
imports: [DepartmentModule],
|
imports: [DepartmentModule],
|
||||||
providers: [StaffService, StaffRouter, StaffRowService, TrpcService],
|
providers: [StaffService, StaffRouter, StaffRowService, TrpcService],
|
||||||
exports: [StaffService, StaffRouter],
|
exports: [StaffService, StaffRouter],
|
||||||
controllers: [StaffController, ],
|
controllers: [StaffController],
|
||||||
})
|
})
|
||||||
export class StaffModule {}
|
export class StaffModule {}
|
||||||
|
|
|
@ -96,7 +96,8 @@ export class StaffRouter {
|
||||||
return await this.staffService.findUnique(input);
|
return await this.staffService.findUnique(input);
|
||||||
}),
|
}),
|
||||||
addCustomField: this.trpc.procedure
|
addCustomField: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
type: z.string(), // text, number, date, select 等
|
type: z.string(), // text, number, date, select 等
|
||||||
|
@ -104,12 +105,14 @@ export class StaffRouter {
|
||||||
order: z.number().optional(),
|
order: z.number().optional(),
|
||||||
options: z.any().optional(), // 对于选择类型字段的可选值
|
options: z.any().optional(), // 对于选择类型字段的可选值
|
||||||
group: z.string().optional(), // 字段分组
|
group: z.string().optional(), // 字段分组
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
return this.staffService.addCustomField(input as any);
|
return this.staffService.addCustomField(input as any);
|
||||||
}),
|
}),
|
||||||
updateCustomField: this.trpc.procedure
|
updateCustomField: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
|
@ -118,7 +121,8 @@ export class StaffRouter {
|
||||||
order: z.number().optional(),
|
order: z.number().optional(),
|
||||||
options: z.any().optional(),
|
options: z.any().optional(),
|
||||||
group: z.string().optional(),
|
group: z.string().optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
return this.staffService.updateCustomField(input as any);
|
return this.staffService.updateCustomField(input as any);
|
||||||
}),
|
}),
|
||||||
|
@ -127,19 +131,19 @@ export class StaffRouter {
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
return this.staffService.deleteCustomField(input as any);
|
return this.staffService.deleteCustomField(input as any);
|
||||||
}),
|
}),
|
||||||
getCustomFields: this.trpc.procedure
|
getCustomFields: this.trpc.procedure.query(() => {
|
||||||
.query(() => {
|
|
||||||
return this.staffService.getCustomFields();
|
return this.staffService.getCustomFields();
|
||||||
}),
|
}),
|
||||||
setCustomFieldValue: this.trpc.procedure
|
setCustomFieldValue: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
staffId: z.string(),
|
staffId: z.string(),
|
||||||
fieldId: z.string(),
|
fieldId: z.string(),
|
||||||
value: z.string().optional(),
|
value: z.string().optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(({ input }) => {
|
.mutation(({ input }) => {
|
||||||
return this.staffService.setCustomFieldValue(input as any);
|
return this.staffService.setCustomFieldValue(input as any);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,10 +180,13 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...staff,
|
...staff,
|
||||||
fieldValues: fieldValues.reduce((acc, { field, value }) => ({
|
fieldValues: fieldValues.reduce(
|
||||||
|
(acc, { field, value }) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[field.name]: value,
|
[field.name]: value,
|
||||||
}), {}),
|
}),
|
||||||
|
{},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,7 +234,33 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
orderBy: { order: 'asc' },
|
orderBy: { order: 'asc' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async findManyWithPagination(params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
where?: Prisma.StaffWhereInput;
|
||||||
|
select?: Prisma.StaffSelect;
|
||||||
|
}) {
|
||||||
|
const { page = 1, pageSize = 10, where, select } = params;
|
||||||
|
const skip = (page - 1) * pageSize;
|
||||||
|
|
||||||
|
const [items, total] = await Promise.all([
|
||||||
|
this.prisma.staff.findMany({
|
||||||
|
skip,
|
||||||
|
take: pageSize,
|
||||||
|
where,
|
||||||
|
select,
|
||||||
|
}),
|
||||||
|
this.prisma.staff.count({ where }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages: Math.ceil(total / pageSize),
|
||||||
|
};
|
||||||
|
}
|
||||||
async setCustomFieldValue(data: {
|
async setCustomFieldValue(data: {
|
||||||
staffId: string;
|
staffId: string;
|
||||||
fieldId: string;
|
fieldId: string;
|
||||||
|
@ -243,7 +272,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
staffId_fieldId: {
|
staffId_fieldId: {
|
||||||
staffId,
|
staffId,
|
||||||
fieldId,
|
fieldId,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
staffId,
|
staffId,
|
||||||
|
@ -255,6 +284,4 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Controller, UseGuards } from "@nestjs/common";
|
import { Controller, UseGuards } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@server/auth/auth.guard';
|
import { AuthGuard } from '@server/auth/auth.guard';
|
||||||
import { SystemLogService } from "./systemLog.service";
|
import { SystemLogService } from './systemLog.service';
|
||||||
|
|
||||||
@Controller('system-logs')
|
@Controller('system-logs')
|
||||||
export class SystemLogController {
|
export class SystemLogController {
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrpcService } from "@server/trpc/trpc.service";
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { SystemLogService } from "./systemLog.service";
|
import { SystemLogService } from './systemLog.service';
|
||||||
import { z, ZodType } from "zod";
|
import { z, ZodType } from 'zod';
|
||||||
import { Prisma } from "@nice/common";
|
import { Prisma } from '@nice/common';
|
||||||
|
|
||||||
// 定义Zod类型Schema
|
// 定义Zod类型Schema
|
||||||
const SystemLogCreateArgsSchema: ZodType<Prisma.SystemLogCreateArgs> = z.any();
|
const SystemLogCreateArgsSchema: ZodType<Prisma.SystemLogCreateArgs> = z.any();
|
||||||
const SystemLogFindManyArgsSchema: ZodType<Prisma.SystemLogFindManyArgs> = z.any();
|
const SystemLogFindManyArgsSchema: ZodType<Prisma.SystemLogFindManyArgs> =
|
||||||
const SystemLogFindUniqueArgsSchema: ZodType<Prisma.SystemLogFindUniqueArgs> = z.any();
|
z.any();
|
||||||
|
const SystemLogFindUniqueArgsSchema: ZodType<Prisma.SystemLogFindUniqueArgs> =
|
||||||
|
z.any();
|
||||||
const SystemLogWhereInputSchema: ZodType<Prisma.SystemLogWhereInput> = z.any();
|
const SystemLogWhereInputSchema: ZodType<Prisma.SystemLogWhereInput> = z.any();
|
||||||
const SystemLogSelectSchema: ZodType<Prisma.SystemLogSelect> = z.any();
|
const SystemLogSelectSchema: ZodType<Prisma.SystemLogSelect> = z.any();
|
||||||
|
|
||||||
|
@ -16,12 +18,13 @@ export class SystemLogRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly systemLogService: SystemLogService,
|
private readonly systemLogService: SystemLogService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
// 创建日志
|
// 创建日志
|
||||||
create: this.trpc.procedure
|
create: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
level: z.enum(['info', 'warning', 'error', 'debug']).default('info'),
|
level: z.enum(['info', 'warning', 'error', 'debug']).default('info'),
|
||||||
module: z.string(),
|
module: z.string(),
|
||||||
action: z.string(),
|
action: z.string(),
|
||||||
|
@ -37,7 +40,8 @@ export class SystemLogRouter {
|
||||||
status: z.enum(['success', 'failure']).default('success'),
|
status: z.enum(['success', 'failure']).default('success'),
|
||||||
errorMessage: z.string().optional(),
|
errorMessage: z.string().optional(),
|
||||||
departmentId: z.string().optional(),
|
departmentId: z.string().optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const ctxIpAddress = ctx.ip;
|
const ctxIpAddress = ctx.ip;
|
||||||
const operatorId = ctx.staff?.id;
|
const operatorId = ctx.staff?.id;
|
||||||
|
@ -60,7 +64,7 @@ export class SystemLogRouter {
|
||||||
status: input.status,
|
status: input.status,
|
||||||
errorMessage: input.errorMessage,
|
errorMessage: input.errorMessage,
|
||||||
departmentId: input.departmentId,
|
departmentId: input.departmentId,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating system log:', error);
|
console.error('Error creating system log:', error);
|
||||||
|
@ -77,12 +81,14 @@ export class SystemLogRouter {
|
||||||
|
|
||||||
// 查询日志列表(带分页) - 保留原名
|
// 查询日志列表(带分页) - 保留原名
|
||||||
getLogs: this.trpc.procedure
|
getLogs: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
page: z.number().default(1),
|
page: z.number().default(1),
|
||||||
pageSize: z.number().default(20),
|
pageSize: z.number().default(20),
|
||||||
where: SystemLogWhereInputSchema.optional(),
|
where: SystemLogWhereInputSchema.optional(),
|
||||||
select: SystemLogSelectSchema.optional(),
|
select: SystemLogSelectSchema.optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const { page, pageSize, where = {}, select } = input;
|
const { page, pageSize, where = {}, select } = input;
|
||||||
|
@ -91,7 +97,7 @@ export class SystemLogRouter {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
where,
|
where,
|
||||||
...(select ? { select } : {})
|
...(select ? { select } : {}),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in getLogs:', error);
|
console.error('Error in getLogs:', error);
|
||||||
|
@ -101,19 +107,21 @@ export class SystemLogRouter {
|
||||||
total: 0,
|
total: 0,
|
||||||
page: input.page,
|
page: input.page,
|
||||||
pageSize: input.pageSize,
|
pageSize: input.pageSize,
|
||||||
totalPages: 0
|
totalPages: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 查询日志列表(带分页) - 新名称
|
// 查询日志列表(带分页) - 新名称
|
||||||
findManyWithPagination: this.trpc.procedure
|
findManyWithPagination: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
page: z.number().default(1),
|
page: z.number().default(1),
|
||||||
pageSize: z.number().default(20),
|
pageSize: z.number().default(20),
|
||||||
where: SystemLogWhereInputSchema.optional(),
|
where: SystemLogWhereInputSchema.optional(),
|
||||||
select: SystemLogSelectSchema.optional(),
|
select: SystemLogSelectSchema.optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const { page, pageSize, where = {}, select } = input;
|
const { page, pageSize, where = {}, select } = input;
|
||||||
|
@ -122,7 +130,7 @@ export class SystemLogRouter {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
where,
|
where,
|
||||||
...(select ? { select } : {})
|
...(select ? { select } : {}),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in findManyWithPagination:', error);
|
console.error('Error in findManyWithPagination:', error);
|
||||||
|
@ -132,7 +140,7 @@ export class SystemLogRouter {
|
||||||
total: 0,
|
total: 0,
|
||||||
page: input.page,
|
page: input.page,
|
||||||
pageSize: input.pageSize,
|
pageSize: input.pageSize,
|
||||||
totalPages: 0
|
totalPages: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -145,9 +153,7 @@ export class SystemLogRouter {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 通过ID获取日志详情(简化版)
|
// 通过ID获取日志详情(简化版)
|
||||||
findById: this.trpc.procedure
|
findById: this.trpc.procedure.input(z.string()).query(async ({ input }) => {
|
||||||
.input(z.string())
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return this.systemLogService.findUnique({
|
return this.systemLogService.findUnique({
|
||||||
where: { id: input },
|
where: { id: input },
|
||||||
include: {
|
include: {
|
||||||
|
@ -156,21 +162,22 @@ export class SystemLogRouter {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
showname: true,
|
showname: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
department: {
|
department: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 记录人员操作日志的便捷方法
|
// 记录人员操作日志的便捷方法
|
||||||
logStaffAction: this.trpc.protectProcedure
|
logStaffAction: this.trpc.protectProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
action: z.string(),
|
action: z.string(),
|
||||||
targetId: z.string(),
|
targetId: z.string(),
|
||||||
targetName: z.string(),
|
targetName: z.string(),
|
||||||
|
@ -179,7 +186,8 @@ export class SystemLogRouter {
|
||||||
afterData: z.any().optional(),
|
afterData: z.any().optional(),
|
||||||
status: z.enum(['success', 'failure']).default('success'),
|
status: z.enum(['success', 'failure']).default('success'),
|
||||||
errorMessage: z.string().optional(),
|
errorMessage: z.string().optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const ipAddress = ctx.ip;
|
const ipAddress = ctx.ip;
|
||||||
const operatorId = ctx.staff?.id;
|
const operatorId = ctx.staff?.id;
|
||||||
|
@ -200,7 +208,7 @@ export class SystemLogRouter {
|
||||||
afterData: input.afterData,
|
afterData: input.afterData,
|
||||||
status: input.status,
|
status: input.status,
|
||||||
errorMessage: input.errorMessage,
|
errorMessage: input.errorMessage,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error logging staff action:', error);
|
console.error('Error logging staff action:', error);
|
||||||
|
@ -210,7 +218,8 @@ export class SystemLogRouter {
|
||||||
|
|
||||||
// 高级搜索日志
|
// 高级搜索日志
|
||||||
searchLogs: this.trpc.procedure
|
searchLogs: this.trpc.procedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
page: z.number().default(1),
|
page: z.number().default(1),
|
||||||
pageSize: z.number().default(20),
|
pageSize: z.number().default(20),
|
||||||
level: z.enum(['info', 'warning', 'error', 'debug']).optional(),
|
level: z.enum(['info', 'warning', 'error', 'debug']).optional(),
|
||||||
|
@ -224,7 +233,8 @@ export class SystemLogRouter {
|
||||||
endTime: z.string().optional(),
|
endTime: z.string().optional(),
|
||||||
keyword: z.string().optional(),
|
keyword: z.string().optional(),
|
||||||
departmentId: z.string().optional(),
|
departmentId: z.string().optional(),
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
console.log('Received input for searchLogs:', input);
|
console.log('Received input for searchLogs:', input);
|
||||||
const where: Prisma.SystemLogWhereInput = {};
|
const where: Prisma.SystemLogWhereInput = {};
|
||||||
|
@ -281,15 +291,15 @@ export class SystemLogRouter {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
showname: true,
|
showname: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
department: {
|
department: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
console.log('Search logs result:', result);
|
console.log('Search logs result:', result);
|
||||||
return result;
|
return result;
|
||||||
|
@ -298,5 +308,5 @@ export class SystemLogRouter {
|
||||||
throw new Error('Failed to search logs');
|
throw new Error('Failed to search logs');
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})
|
});
|
||||||
}
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { BaseService } from "../base/base.service";
|
import { BaseService } from '../base/base.service';
|
||||||
import { db, ObjectType, Prisma } from "@nice/common";
|
import { db, ObjectType, Prisma } from '@nice/common';
|
||||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
|
@ -21,7 +21,9 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
|
|
||||||
// 添加默认消息格式 - 确保 message 字段存在
|
// 添加默认消息格式 - 确保 message 字段存在
|
||||||
if (!args.data.message) {
|
if (!args.data.message) {
|
||||||
args.data.message = `[${timestamp}] ${messagePrefix}${module || ''} ${action || ''}: ${targetName || ''}`;
|
args.data.message = `[${timestamp}] ${messagePrefix}${module || ''} ${
|
||||||
|
action || ''
|
||||||
|
}: ${targetName || ''}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,11 +32,18 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMany(args: Prisma.SystemLogFindManyArgs): Promise<Prisma.SystemLogGetPayload<{}>[]> {
|
async findMany(
|
||||||
|
args: Prisma.SystemLogFindManyArgs,
|
||||||
|
): Promise<Prisma.SystemLogGetPayload<{}>[]> {
|
||||||
return super.findMany(args); // 放弃分页结构
|
return super.findMany(args); // 放弃分页结构
|
||||||
}
|
}
|
||||||
|
|
||||||
async findManyWithPagination({ page = 1, pageSize = 20, where = {}, ...rest }: any) {
|
async findManyWithPagination({
|
||||||
|
page = 1,
|
||||||
|
pageSize = 20,
|
||||||
|
where = {},
|
||||||
|
...rest
|
||||||
|
}: any) {
|
||||||
const skip = (page - 1) * pageSize;
|
const skip = (page - 1) * pageSize;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -44,9 +53,9 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
skip,
|
skip,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
orderBy: { timestamp: 'desc' },
|
orderBy: { timestamp: 'desc' },
|
||||||
...rest
|
...rest,
|
||||||
}),
|
}),
|
||||||
this.prismaClient.systemLog.count({ where })
|
this.prismaClient.systemLog.count({ where }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -54,7 +63,7 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
totalPages: Math.ceil(total / pageSize)
|
totalPages: Math.ceil(total / pageSize),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in findManyWithPagination:', error);
|
console.error('Error in findManyWithPagination:', error);
|
||||||
|
@ -71,10 +80,11 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
beforeData: any = null,
|
beforeData: any = null,
|
||||||
afterData: any = null,
|
afterData: any = null,
|
||||||
status: 'success' | 'failure' = 'success',
|
status: 'success' | 'failure' = 'success',
|
||||||
errorMessage?: string
|
errorMessage?: string,
|
||||||
) {
|
) {
|
||||||
// 生成变更详情
|
// 生成变更详情
|
||||||
const details = beforeData && afterData
|
const details =
|
||||||
|
beforeData && afterData
|
||||||
? this.generateChangeDetails(beforeData, afterData)
|
? this.generateChangeDetails(beforeData, afterData)
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
@ -98,7 +108,7 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
afterData,
|
afterData,
|
||||||
status,
|
status,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,14 +120,15 @@ export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
|
||||||
|
|
||||||
const changes: Record<string, { oldValue: any; newValue: any }> = {};
|
const changes: Record<string, { oldValue: any; newValue: any }> = {};
|
||||||
|
|
||||||
Object.keys(after).forEach(key => {
|
Object.keys(after).forEach((key) => {
|
||||||
// 忽略一些不需要记录的字段
|
// 忽略一些不需要记录的字段
|
||||||
if (['password', 'createdAt', 'updatedAt', 'deletedAt'].includes(key)) return;
|
if (['password', 'createdAt', 'updatedAt', 'deletedAt'].includes(key))
|
||||||
|
return;
|
||||||
|
|
||||||
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
||||||
changes[key] = {
|
changes[key] = {
|
||||||
oldValue: before[key],
|
oldValue: before[key],
|
||||||
newValue: after[key]
|
newValue: after[key],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ export class TaxonomyRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly taxonomyService: TaxonomyService,
|
private readonly taxonomyService: TaxonomyService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.procedure
|
create: this.trpc.procedure
|
||||||
|
|
|
@ -13,4 +13,4 @@ import { TermRowService } from './term.row.service';
|
||||||
exports: [TermService, TermRouter],
|
exports: [TermService, TermRouter],
|
||||||
controllers: [TermController],
|
controllers: [TermController],
|
||||||
})
|
})
|
||||||
export class TermModule { }
|
export class TermModule {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Controller, UseGuards } from "@nestjs/common";
|
import { Controller, UseGuards } from '@nestjs/common';
|
||||||
import { TrainContentService } from "./trainContent.service";
|
import { TrainContentService } from './trainContent.service';
|
||||||
import { AuthGuard } from '@server/auth/auth.guard';
|
import { AuthGuard } from '@server/auth/auth.guard';
|
||||||
|
|
||||||
@Controller('train-content')
|
@Controller('train-content')
|
||||||
|
|
|
@ -5,11 +5,10 @@ import { TrainContentController } from './trainContent.controller';
|
||||||
import { TrainContentRouter } from './trainContent.router';
|
import { TrainContentRouter } from './trainContent.router';
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StaffModule],
|
imports: [StaffModule],
|
||||||
controllers: [TrainContentController],
|
controllers: [TrainContentController],
|
||||||
providers: [TrainContentService,TrainContentRouter,TrpcService],
|
providers: [TrainContentService, TrainContentRouter, TrpcService],
|
||||||
exports: [TrainContentService,TrainContentRouter],
|
exports: [TrainContentService, TrainContentRouter],
|
||||||
})
|
})
|
||||||
export class TrainContentModule {}
|
export class TrainContentModule {}
|
|
@ -1,32 +1,36 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrpcService } from "@server/trpc/trpc.service";
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { TrainContentService } from "./trainContent.service";
|
import { TrainContentService } from './trainContent.service';
|
||||||
import { z, ZodType } from "zod";
|
import { z, ZodType } from 'zod';
|
||||||
import { Prisma } from "@nice/common";
|
import { Prisma } from '@nice/common';
|
||||||
|
|
||||||
const TrainContentArgsSchema:ZodType<Prisma.TrainContentCreateArgs> = z.any()
|
const TrainContentArgsSchema: ZodType<Prisma.TrainContentCreateArgs> = z.any();
|
||||||
const TrainContentUpdateArgsSchema:ZodType<Prisma.TrainContentUpdateArgs> = z.any()
|
const TrainContentUpdateArgsSchema: ZodType<Prisma.TrainContentUpdateArgs> =
|
||||||
const TrainContentFindManyArgsSchema:ZodType<Prisma.TrainContentFindManyArgs> = z.any()
|
z.any();
|
||||||
|
const TrainContentFindManyArgsSchema: ZodType<Prisma.TrainContentFindManyArgs> =
|
||||||
|
z.any();
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrainContentRouter {
|
export class TrainContentRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly trainContentService: TrainContentService,
|
private readonly trainContentService: TrainContentService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create:this.trpc.procedure.input(TrainContentArgsSchema)
|
create: this.trpc.procedure
|
||||||
.mutation(async ({input})=>{
|
.input(TrainContentArgsSchema)
|
||||||
return this.trainContentService.create(input)
|
.mutation(async ({ input }) => {
|
||||||
|
return this.trainContentService.create(input);
|
||||||
}),
|
}),
|
||||||
update:this.trpc.procedure.input(TrainContentUpdateArgsSchema)
|
update: this.trpc.procedure
|
||||||
.mutation(async ({input})=>{
|
.input(TrainContentUpdateArgsSchema)
|
||||||
return this.trainContentService.update(input)
|
.mutation(async ({ input }) => {
|
||||||
|
return this.trainContentService.update(input);
|
||||||
}),
|
}),
|
||||||
findMany:this.trpc.procedure.input(TrainContentFindManyArgsSchema)
|
findMany: this.trpc.procedure
|
||||||
.query(async ({input})=>{
|
.input(TrainContentFindManyArgsSchema)
|
||||||
return this.trainContentService.findMany(input)
|
.query(async ({ input }) => {
|
||||||
})
|
return this.trainContentService.findMany(input);
|
||||||
})
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
|
@ -1,38 +1,35 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { BaseService } from "../base/base.service";
|
import { BaseService } from '../base/base.service';
|
||||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrainContentService extends BaseService<Prisma.TrainContentDelegate> {
|
export class TrainContentService extends BaseService<Prisma.TrainContentDelegate> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db,ObjectType.TRAIN_CONTENT,true);
|
super(db, ObjectType.TRAIN_CONTENT, true);
|
||||||
}
|
}
|
||||||
async create(args: Prisma.TrainContentCreateArgs) {
|
async create(args: Prisma.TrainContentCreateArgs) {
|
||||||
console.log(args)
|
console.log(args);
|
||||||
const result = await super.create(args)
|
const result = await super.create(args);
|
||||||
this.emitDataChanged(CrudOperation.CREATED,result)
|
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(args:Prisma.TrainContentUpdateArgs){
|
async update(args: Prisma.TrainContentUpdateArgs) {
|
||||||
const result = await super.update(args)
|
const result = await super.update(args);
|
||||||
this.emitDataChanged(CrudOperation.UPDATED,result)
|
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async findMany(args: Prisma.TrainContentFindManyArgs) {
|
async findMany(args: Prisma.TrainContentFindManyArgs) {
|
||||||
const result = await super.findMany(args);
|
const result = await super.findMany(args);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||||
EventBus.emit('dataChanged', {
|
EventBus.emit('dataChanged', {
|
||||||
type:ObjectType.TRAIN_SITUATION,
|
type: ObjectType.TRAIN_SITUATION,
|
||||||
operation,
|
operation,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Controller, UseGuards } from "@nestjs/common";
|
import { Controller, UseGuards } from '@nestjs/common';
|
||||||
import { TrainSituationService } from "./trainSituation.service";
|
import { TrainSituationService } from './trainSituation.service';
|
||||||
import { AuthGuard } from '@server/auth/auth.guard';
|
import { AuthGuard } from '@server/auth/auth.guard';
|
||||||
|
|
||||||
@Controller('train-situation')
|
@Controller('train-situation')
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StaffModule],
|
imports: [StaffModule],
|
||||||
controllers: [TrainSituationController],
|
controllers: [TrainSituationController],
|
||||||
providers: [TrainSituationService,TrainSituationRouter,TrpcService],
|
providers: [TrainSituationService, TrainSituationRouter, TrpcService],
|
||||||
exports: [TrainSituationService,TrainSituationRouter],
|
exports: [TrainSituationService, TrainSituationRouter],
|
||||||
})
|
})
|
||||||
export class TrainSituationModule {}
|
export class TrainSituationModule {}
|
|
@ -1,45 +1,49 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { TrpcService } from "@server/trpc/trpc.service";
|
import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import { TrainSituationService } from "./trainSituation.service";
|
import { TrainSituationService } from './trainSituation.service';
|
||||||
import { z, ZodType } from "zod";
|
import { z, ZodType } from 'zod';
|
||||||
import { Prisma } from "@nice/common";
|
import { Prisma } from '@nice/common';
|
||||||
|
|
||||||
const TrainSituationArgsSchema:ZodType<Prisma.TrainSituationCreateArgs> = z.any()
|
const TrainSituationArgsSchema: ZodType<Prisma.TrainSituationCreateArgs> =
|
||||||
const TrainSituationUpdateArgsSchema:ZodType<Prisma.TrainSituationUpdateArgs> = z.any()
|
z.any();
|
||||||
const TrainSituationFindManyArgsSchema:ZodType<Prisma.TrainSituationFindManyArgs> = z.any()
|
const TrainSituationUpdateArgsSchema: ZodType<Prisma.TrainSituationUpdateArgs> =
|
||||||
|
z.any();
|
||||||
|
const TrainSituationFindManyArgsSchema: ZodType<Prisma.TrainSituationFindManyArgs> =
|
||||||
|
z.any();
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrainSituationRouter {
|
export class TrainSituationRouter {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly trainSituationService: TrainSituationService,
|
private readonly trainSituationService: TrainSituationService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create:this.trpc.protectProcedure
|
create: this.trpc.protectProcedure
|
||||||
.input(TrainSituationArgsSchema)
|
.input(TrainSituationArgsSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return this.trainSituationService.create(input)
|
return this.trainSituationService.create(input);
|
||||||
}),
|
}),
|
||||||
update:this.trpc.protectProcedure
|
update: this.trpc.protectProcedure
|
||||||
.input(TrainSituationUpdateArgsSchema)
|
.input(TrainSituationUpdateArgsSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return this.trainSituationService.update(input)
|
return this.trainSituationService.update(input);
|
||||||
}),
|
}),
|
||||||
findMany:this.trpc.protectProcedure
|
findMany: this.trpc.protectProcedure
|
||||||
.input(TrainSituationFindManyArgsSchema)
|
.input(TrainSituationFindManyArgsSchema)
|
||||||
.query(async ({input}) =>{
|
.query(async ({ input }) => {
|
||||||
return this.trainSituationService.findMany(input)
|
return this.trainSituationService.findMany(input);
|
||||||
}),
|
}),
|
||||||
findManyByDepId:this.trpc.protectProcedure
|
findManyByDepId: this.trpc.protectProcedure
|
||||||
.input(z.object({
|
.input(
|
||||||
|
z.object({
|
||||||
deptId: z.string().optional(),
|
deptId: z.string().optional(),
|
||||||
domainId: z.string().optional(),
|
domainId: z.string().optional(),
|
||||||
trainContentId: z.string().optional()
|
trainContentId: z.string().optional(),
|
||||||
}))
|
}),
|
||||||
.query(async ({input}) => {
|
)
|
||||||
return this.trainSituationService.findManyByDeptId(input)
|
.query(async ({ input }) => {
|
||||||
})
|
return this.trainSituationService.findManyByDeptId(input);
|
||||||
})
|
}),
|
||||||
|
});
|
||||||
}
|
}
|
|
@ -1,78 +1,78 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { BaseService } from "../base/base.service";
|
import { BaseService } from '../base/base.service';
|
||||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||||
import { StaffService } from "../staff/staff.service";
|
import { StaffService } from '../staff/staff.service';
|
||||||
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrainSituationService extends BaseService<Prisma.TrainSituationDelegate> {
|
export class TrainSituationService extends BaseService<Prisma.TrainSituationDelegate> {
|
||||||
constructor(private readonly staffService:StaffService) {
|
constructor(private readonly staffService: StaffService) {
|
||||||
super(db,ObjectType.TRAIN_SITUATION,false);
|
super(db, ObjectType.TRAIN_SITUATION, false);
|
||||||
}
|
}
|
||||||
// 创建培训情况
|
// 创建培训情况
|
||||||
async create(
|
async create(args: Prisma.TrainSituationCreateArgs) {
|
||||||
args: Prisma.TrainSituationCreateArgs,
|
console.log(args);
|
||||||
){
|
|
||||||
console.log(args)
|
|
||||||
const result = await super.create(args);
|
const result = await super.create(args);
|
||||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// 更新培训情况
|
// 更新培训情况
|
||||||
async update(
|
async update(args: Prisma.TrainSituationUpdateArgs) {
|
||||||
args: Prisma.TrainSituationUpdateArgs,
|
|
||||||
){
|
|
||||||
const result = await super.update(args);
|
const result = await super.update(args);
|
||||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找培训情况
|
// 查找培训情况
|
||||||
async findMany(args: Prisma.TrainSituationFindManyArgs): Promise<{
|
async findMany(args: Prisma.TrainSituationFindManyArgs): Promise<
|
||||||
|
{
|
||||||
id: string;
|
id: string;
|
||||||
staffId: string;
|
staffId: string;
|
||||||
trainContentId: string;
|
trainContentId: string;
|
||||||
mustTrainTime: number;
|
mustTrainTime: number;
|
||||||
alreadyTrainTime: number;
|
alreadyTrainTime: number;
|
||||||
score: number;
|
score: number;
|
||||||
}[]>
|
}[]
|
||||||
{
|
> {
|
||||||
const result = await super.findMany(args);
|
const result = await super.findMany(args);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
// 查找某一单位所有人员的培训情况
|
// 查找某一单位所有人员的培训情况
|
||||||
async findManyByDeptId(args:{
|
async findManyByDeptId(args: {
|
||||||
deptId?:string,
|
deptId?: string;
|
||||||
domainId?:string,
|
domainId?: string;
|
||||||
trainContentId?:string
|
trainContentId?: string;
|
||||||
}):Promise<{
|
}): Promise<
|
||||||
|
{
|
||||||
id: string;
|
id: string;
|
||||||
staffId: string;
|
staffId: string;
|
||||||
trainContentId: string;
|
trainContentId: string;
|
||||||
mustTrainTime: number;
|
mustTrainTime: number;
|
||||||
alreadyTrainTime: number;
|
alreadyTrainTime: number;
|
||||||
score: number;
|
score: number;
|
||||||
}[]>
|
}[]
|
||||||
{
|
> {
|
||||||
const staffs = await this.staffService.findByDept({deptId:args.deptId,domainId:args.domainId})
|
const staffs = await this.staffService.findByDept({
|
||||||
|
deptId: args.deptId,
|
||||||
|
domainId: args.domainId,
|
||||||
|
});
|
||||||
const result = await super.findMany({
|
const result = await super.findMany({
|
||||||
where:{
|
where: {
|
||||||
staffId:{
|
staffId: {
|
||||||
in:staffs.map(staff=>staff.id)
|
in: staffs.map((staff) => staff.id),
|
||||||
},
|
},
|
||||||
...(args.trainContentId ? {trainContentId:args.trainContentId} : {})
|
...(args.trainContentId ? { trainContentId: args.trainContentId } : {}),
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
//async createDailyTrainTime()
|
//async createDailyTrainTime()
|
||||||
|
|
||||||
// 发送数据变化事件
|
// 发送数据变化事件
|
||||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||||
EventBus.emit('dataChanged', {
|
EventBus.emit('dataChanged', {
|
||||||
type:ObjectType.TRAIN_SITUATION,
|
type: ObjectType.TRAIN_SITUATION,
|
||||||
operation,
|
operation,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,12 +8,7 @@ import { DepartmentModule } from '../department/department.module';
|
||||||
import { StaffModule } from '../staff/staff.module';
|
import { StaffModule } from '../staff/staff.module';
|
||||||
// import { TransformController } from './transform.controller';
|
// import { TransformController } from './transform.controller';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [DepartmentModule, StaffModule, TermModule, TaxonomyModule],
|
||||||
DepartmentModule,
|
|
||||||
StaffModule,
|
|
||||||
TermModule,
|
|
||||||
TaxonomyModule,
|
|
||||||
],
|
|
||||||
providers: [TransformService, TransformRouter, TrpcService],
|
providers: [TransformService, TransformRouter, TrpcService],
|
||||||
exports: [TransformRouter, TransformService],
|
exports: [TransformRouter, TransformService],
|
||||||
// controllers:[TransformController]
|
// controllers:[TransformController]
|
||||||
|
|
|
@ -5,6 +5,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [VisitService, VisitRouter, TrpcService],
|
providers: [VisitService, VisitRouter, TrpcService],
|
||||||
exports: [VisitRouter]
|
exports: [VisitRouter],
|
||||||
})
|
})
|
||||||
export class VisitModule { }
|
export class VisitModule {}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
import { WebSocketServer, WebSocket } from "ws";
|
import { Logger } from '@nestjs/common';
|
||||||
import { Logger } from "@nestjs/common";
|
import { WebSocketServerConfig, WSClient, WebSocketType } from '../types';
|
||||||
import { WebSocketServerConfig, WSClient, WebSocketType } from "../types";
|
|
||||||
import { SocketMessage } from '@nice/common';
|
import { SocketMessage } from '@nice/common';
|
||||||
|
|
||||||
const DEFAULT_CONFIG: WebSocketServerConfig = {
|
const DEFAULT_CONFIG: WebSocketServerConfig = {
|
||||||
|
@ -25,9 +24,7 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
protected readonly logger = new Logger(this.constructor.name);
|
protected readonly logger = new Logger(this.constructor.name);
|
||||||
protected readonly finalConfig: WebSocketServerConfig;
|
protected readonly finalConfig: WebSocketServerConfig;
|
||||||
private userClientMap: Map<string, WSClient> = new Map();
|
private userClientMap: Map<string, WSClient> = new Map();
|
||||||
constructor(
|
constructor(protected readonly config: Partial<WebSocketServerConfig> = {}) {
|
||||||
protected readonly config: Partial<WebSocketServerConfig> = {}
|
|
||||||
) {
|
|
||||||
this.finalConfig = {
|
this.finalConfig = {
|
||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
...config,
|
...config,
|
||||||
|
@ -39,7 +36,7 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public getClientCount() {
|
public getClientCount() {
|
||||||
return this.clients.size
|
return this.clients.size;
|
||||||
}
|
}
|
||||||
// 暴露 WebSocketServer 实例的只读访问
|
// 暴露 WebSocketServer 实例的只读访问
|
||||||
public get wss(): WebSocketServer | null {
|
public get wss(): WebSocketServer | null {
|
||||||
|
@ -62,7 +59,7 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
|
|
||||||
this._wss = new WebSocketServer({
|
this._wss = new WebSocketServer({
|
||||||
noServer: true,
|
noServer: true,
|
||||||
path: this.serverPath
|
path: this.serverPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.debugLog(`WebSocket server starting on path: ${this.serverPath}`);
|
this.debugLog(`WebSocket server starting on path: ${this.serverPath}`);
|
||||||
|
@ -76,12 +73,12 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
this.pingIntervalId = undefined;
|
this.pingIntervalId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clients.forEach(client => client.close());
|
this.clients.forEach((client) => client.close());
|
||||||
this.clients.clear();
|
this.clients.clear();
|
||||||
this.timeouts.clear();
|
this.timeouts.clear();
|
||||||
|
|
||||||
if (this._wss) {
|
if (this._wss) {
|
||||||
await new Promise(resolve => this._wss!.close(resolve));
|
await new Promise((resolve) => this._wss!.close(resolve));
|
||||||
this._wss = null;
|
this._wss = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,33 +86,36 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public broadcast(data: SocketMessage): void {
|
public broadcast(data: SocketMessage): void {
|
||||||
this.clients.forEach(client =>
|
this.clients.forEach(
|
||||||
client.readyState === WebSocket.OPEN && client.send(JSON.stringify(data))
|
(client) =>
|
||||||
|
client.readyState === WebSocket.OPEN &&
|
||||||
|
client.send(JSON.stringify(data)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
public sendToUser(id: string, data: SocketMessage) {
|
public sendToUser(id: string, data: SocketMessage) {
|
||||||
const message = JSON.stringify(data);
|
const message = JSON.stringify(data);
|
||||||
const client = this.userClientMap.get(id);
|
const client = this.userClientMap.get(id);
|
||||||
client?.send(message)
|
client?.send(message);
|
||||||
}
|
}
|
||||||
public sendToUsers(ids: string[], data: SocketMessage) {
|
public sendToUsers(ids: string[], data: SocketMessage) {
|
||||||
const message = JSON.stringify(data);
|
const message = JSON.stringify(data);
|
||||||
ids.forEach(id => {
|
ids.forEach((id) => {
|
||||||
const client = this.userClientMap.get(id);
|
const client = this.userClientMap.get(id);
|
||||||
client?.send(message);
|
client?.send(message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public sendToRoom(roomId: string, data: SocketMessage) {
|
public sendToRoom(roomId: string, data: SocketMessage) {
|
||||||
const message = JSON.stringify(data);
|
const message = JSON.stringify(data);
|
||||||
this.clients.forEach(client => {
|
this.clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN && client.roomId === roomId) {
|
if (client.readyState === WebSocket.OPEN && client.roomId === roomId) {
|
||||||
client.send(message)
|
client.send(message);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
protected getRoomClientsCount(roomId?: string): number {
|
protected getRoomClientsCount(roomId?: string): number {
|
||||||
if (!roomId) return 0;
|
if (!roomId) return 0;
|
||||||
return Array.from(this.clients).filter(client => client.roomId === roomId).length;
|
return Array.from(this.clients).filter((client) => client.roomId === roomId)
|
||||||
|
.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
public handleConnection(ws: WSClient): void {
|
public handleConnection(ws: WSClient): void {
|
||||||
|
@ -161,7 +161,10 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
ws.on('pong', () => this.handlePong(ws))
|
ws.on('pong', () => this.handlePong(ws))
|
||||||
.on('close', () => this.handleDisconnection(ws))
|
.on('close', () => this.handleDisconnection(ws))
|
||||||
.on('error', (error) => {
|
.on('error', (error) => {
|
||||||
this.logger.error(`[${this.serverType}] client error on path ${this.serverPath}:`, error);
|
this.logger.error(
|
||||||
|
`[${this.serverType}] client error on path ${this.serverPath}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
this.handleDisconnection(ws);
|
this.handleDisconnection(ws);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -178,19 +181,19 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
private startPingInterval(): void {
|
private startPingInterval(): void {
|
||||||
this.pingIntervalId = setInterval(
|
this.pingIntervalId = setInterval(
|
||||||
() => this.pingClients(),
|
() => this.pingClients(),
|
||||||
this.finalConfig.pingInterval
|
this.finalConfig.pingInterval,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private pingClients(): void {
|
private pingClients(): void {
|
||||||
this.clients.forEach(ws => {
|
this.clients.forEach((ws) => {
|
||||||
if (!ws.isAlive) return this.handleDisconnection(ws);
|
if (!ws.isAlive) return this.handleDisconnection(ws);
|
||||||
|
|
||||||
ws.isAlive = false;
|
ws.isAlive = false;
|
||||||
ws.ping();
|
ws.ping();
|
||||||
const timeout = setTimeout(
|
const timeout = setTimeout(
|
||||||
() => !ws.isAlive && this.handleDisconnection(ws),
|
() => !ws.isAlive && this.handleDisconnection(ws),
|
||||||
this.finalConfig.pingTimeout
|
this.finalConfig.pingTimeout,
|
||||||
);
|
);
|
||||||
this.timeouts.set(ws, timeout);
|
this.timeouts.set(ws, timeout);
|
||||||
});
|
});
|
||||||
|
@ -200,6 +203,8 @@ export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||||
if (!this._wss) return;
|
if (!this._wss) return;
|
||||||
this._wss
|
this._wss
|
||||||
.on('connection', (ws: WSClient) => this.handleConnection(ws))
|
.on('connection', (ws: WSClient) => this.handleConnection(ws))
|
||||||
.on('error', (error) => this.logger.error(`Server error on path ${this.serverPath}:`, error));
|
.on('error', (error) =>
|
||||||
|
this.logger.error(`Server error on path ${this.serverPath}:`, error),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,13 @@ import http from 'http';
|
||||||
import { parseInt as libParseInt } from 'lib0/number';
|
import { parseInt as libParseInt } from 'lib0/number';
|
||||||
import { WSSharedDoc } from './ws-shared-doc';
|
import { WSSharedDoc } from './ws-shared-doc';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回调URL配置,从环境变量中获取
|
* 回调URL配置,从环境变量中获取
|
||||||
* 如果环境变量未设置则为null
|
* 如果环境变量未设置则为null
|
||||||
*/
|
*/
|
||||||
const CALLBACK_URL = process.env.CALLBACK_URL ? new URL(process.env.CALLBACK_URL) : null;
|
const CALLBACK_URL = process.env.CALLBACK_URL
|
||||||
|
? new URL(process.env.CALLBACK_URL)
|
||||||
|
: null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 回调超时时间配置,从环境变量中获取
|
* 回调超时时间配置,从环境变量中获取
|
||||||
|
@ -25,7 +26,9 @@ const CALLBACK_TIMEOUT = libParseInt(process.env.CALLBACK_TIMEOUT || '5000');
|
||||||
* 需要监听变更的共享对象配置
|
* 需要监听变更的共享对象配置
|
||||||
* 从环境变量CALLBACK_OBJECTS中解析JSON格式的配置
|
* 从环境变量CALLBACK_OBJECTS中解析JSON格式的配置
|
||||||
*/
|
*/
|
||||||
const CALLBACK_OBJECTS: Record<string, string> = process.env.CALLBACK_OBJECTS ? JSON.parse(process.env.CALLBACK_OBJECTS) : {};
|
const CALLBACK_OBJECTS: Record<string, string> = process.env.CALLBACK_OBJECTS
|
||||||
|
? JSON.parse(process.env.CALLBACK_OBJECTS)
|
||||||
|
: {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出回调URL是否已配置的标志
|
* 导出回调URL是否已配置的标志
|
||||||
|
@ -37,10 +40,13 @@ export const isCallbackSet = !!CALLBACK_URL;
|
||||||
*/
|
*/
|
||||||
interface DataToSend {
|
interface DataToSend {
|
||||||
room: string; // 房间/文档标识
|
room: string; // 房间/文档标识
|
||||||
data: Record<string, {
|
data: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
type: string; // 数据类型
|
type: string; // 数据类型
|
||||||
content: any; // 数据内容
|
content: any; // 数据内容
|
||||||
}>;
|
}
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,25 +65,29 @@ type OriginType = any;
|
||||||
* @param origin - 更新的来源
|
* @param origin - 更新的来源
|
||||||
* @param doc - 共享文档实例
|
* @param doc - 共享文档实例
|
||||||
*/
|
*/
|
||||||
export const callbackHandler = (update: UpdateType, origin: OriginType, doc: WSSharedDoc): void => {
|
export const callbackHandler = (
|
||||||
|
update: UpdateType,
|
||||||
|
origin: OriginType,
|
||||||
|
doc: WSSharedDoc,
|
||||||
|
): void => {
|
||||||
// 获取文档名称作为房间标识
|
// 获取文档名称作为房间标识
|
||||||
const room = doc.name;
|
const room = doc.name;
|
||||||
|
|
||||||
// 初始化要发送的数据对象
|
// 初始化要发送的数据对象
|
||||||
const dataToSend: DataToSend = {
|
const dataToSend: DataToSend = {
|
||||||
room,
|
room,
|
||||||
data: {}
|
data: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取所有需要监听的共享对象名称
|
// 获取所有需要监听的共享对象名称
|
||||||
const sharedObjectList = Object.keys(CALLBACK_OBJECTS);
|
const sharedObjectList = Object.keys(CALLBACK_OBJECTS);
|
||||||
|
|
||||||
// 遍历所有共享对象,获取它们的最新内容
|
// 遍历所有共享对象,获取它们的最新内容
|
||||||
sharedObjectList.forEach(sharedObjectName => {
|
sharedObjectList.forEach((sharedObjectName) => {
|
||||||
const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName];
|
const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName];
|
||||||
dataToSend.data[sharedObjectName] = {
|
dataToSend.data[sharedObjectName] = {
|
||||||
type: sharedObjectType,
|
type: sharedObjectType,
|
||||||
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON()
|
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -106,8 +116,8 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Content-Length': Buffer.byteLength(dataString)
|
'Content-Length': Buffer.byteLength(dataString),
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建HTTP请求
|
// 创建HTTP请求
|
||||||
|
@ -137,14 +147,24 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
|
||||||
* @param doc - 共享文档实例
|
* @param doc - 共享文档实例
|
||||||
* @returns 共享对象的内容
|
* @returns 共享对象的内容
|
||||||
*/
|
*/
|
||||||
const getContent = (objName: string, objType: string, doc: WSSharedDoc): any => {
|
const getContent = (
|
||||||
|
objName: string,
|
||||||
|
objType: string,
|
||||||
|
doc: WSSharedDoc,
|
||||||
|
): any => {
|
||||||
// 根据对象类型返回相应的共享对象
|
// 根据对象类型返回相应的共享对象
|
||||||
switch (objType) {
|
switch (objType) {
|
||||||
case 'Array': return doc.getArray(objName);
|
case 'Array':
|
||||||
case 'Map': return doc.getMap(objName);
|
return doc.getArray(objName);
|
||||||
case 'Text': return doc.getText(objName);
|
case 'Map':
|
||||||
case 'XmlFragment': return doc.getXmlFragment(objName);
|
return doc.getMap(objName);
|
||||||
case 'XmlElement': return doc.getXmlElement(objName);
|
case 'Text':
|
||||||
default: return {};
|
return doc.getText(objName);
|
||||||
|
case 'XmlFragment':
|
||||||
|
return doc.getXmlFragment(objName);
|
||||||
|
case 'XmlElement':
|
||||||
|
return doc.getXmlElement(objName);
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,6 @@ import { YjsServer } from './yjs.server';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [YjsServer],
|
providers: [YjsServer],
|
||||||
exports: [YjsServer]
|
exports: [YjsServer],
|
||||||
})
|
})
|
||||||
export class CollaborationModule { }
|
export class CollaborationModule {}
|
||||||
|
|
|
@ -23,7 +23,7 @@ if (typeof persistenceDir === 'string') {
|
||||||
ldb.storeUpdate(docName, update);
|
ldb.storeUpdate(docName, update);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
writeState: async (_docName, _ydoc) => { },
|
writeState: async (_docName, _ydoc) => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export interface ConnectionOptions {
|
export interface ConnectionOptions {
|
||||||
docName: string;
|
docName: string;
|
||||||
gc: boolean;
|
gc: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
import { readSyncMessage } from '@nice/common';
|
import { readSyncMessage } from '@nice/common';
|
||||||
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates, writeSyncStep1, writeUpdate } from '@nice/common';
|
import {
|
||||||
|
applyAwarenessUpdate,
|
||||||
|
Awareness,
|
||||||
|
encodeAwarenessUpdate,
|
||||||
|
removeAwarenessStates,
|
||||||
|
writeSyncStep1,
|
||||||
|
writeUpdate,
|
||||||
|
} from '@nice/common';
|
||||||
import * as encoding from 'lib0/encoding';
|
import * as encoding from 'lib0/encoding';
|
||||||
import * as decoding from 'lib0/decoding';
|
import * as decoding from 'lib0/decoding';
|
||||||
import * as Y from "yjs"
|
import * as Y from 'yjs';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { getPersistence, setPersistence } from './persistence';
|
import { getPersistence, setPersistence } from './persistence';
|
||||||
import { callbackHandler, isCallbackSet } from './callback';
|
import { callbackHandler, isCallbackSet } from './callback';
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from 'ws';
|
||||||
import { YMessageType } from '@nice/common';
|
import { YMessageType } from '@nice/common';
|
||||||
import { WSClient } from '../types';
|
import { WSClient } from '../types';
|
||||||
export const docs = new Map<string, WSSharedDoc>();
|
export const docs = new Map<string, WSSharedDoc>();
|
||||||
export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000');
|
export const CALLBACK_DEBOUNCE_WAIT = parseInt(
|
||||||
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000');
|
process.env.CALLBACK_DEBOUNCE_WAIT || '2000',
|
||||||
|
);
|
||||||
|
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(
|
||||||
|
process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000',
|
||||||
|
);
|
||||||
export const getYDoc = (docname: string, gc = true): WSSharedDoc => {
|
export const getYDoc = (docname: string, gc = true): WSSharedDoc => {
|
||||||
return docs.get(docname) || createYDoc(docname, gc);
|
return docs.get(docname) || createYDoc(docname, gc);
|
||||||
};
|
};
|
||||||
|
@ -27,7 +38,9 @@ export const send = (doc: WSSharedDoc, conn: WebSocket, m: Uint8Array) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
conn.send(m, {}, err => { err != null && closeConn(doc, conn) });
|
conn.send(m, {}, (err) => {
|
||||||
|
err != null && closeConn(doc, conn);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
closeConn(doc, conn);
|
closeConn(doc, conn);
|
||||||
}
|
}
|
||||||
|
@ -36,14 +49,12 @@ export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => {
|
||||||
if (doc.conns.has(conn)) {
|
if (doc.conns.has(conn)) {
|
||||||
const controlledIds = doc.conns.get(conn) as Set<number>;
|
const controlledIds = doc.conns.get(conn) as Set<number>;
|
||||||
doc.conns.delete(conn);
|
doc.conns.delete(conn);
|
||||||
removeAwarenessStates(
|
removeAwarenessStates(doc.awareness, Array.from(controlledIds), null);
|
||||||
doc.awareness,
|
|
||||||
Array.from(controlledIds),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (doc.conns.size === 0 && getPersistence() !== null) {
|
if (doc.conns.size === 0 && getPersistence() !== null) {
|
||||||
getPersistence()?.writeState(doc.name, doc).then(() => {
|
getPersistence()
|
||||||
|
?.writeState(doc.name, doc)
|
||||||
|
.then(() => {
|
||||||
doc.destroy();
|
doc.destroy();
|
||||||
});
|
});
|
||||||
docs.delete(doc.name);
|
docs.delete(doc.name);
|
||||||
|
@ -52,7 +63,11 @@ export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => {
|
||||||
conn.close();
|
conn.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8Array) => {
|
export const messageListener = (
|
||||||
|
conn: WSClient,
|
||||||
|
doc: WSSharedDoc,
|
||||||
|
message: Uint8Array,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const encoder = encoding.createEncoder();
|
const encoder = encoding.createEncoder();
|
||||||
const decoder = decoding.createDecoder(message);
|
const decoder = decoding.createDecoder(message);
|
||||||
|
@ -71,7 +86,7 @@ export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8
|
||||||
applyAwarenessUpdate(
|
applyAwarenessUpdate(
|
||||||
doc.awareness,
|
doc.awareness,
|
||||||
decoding.readVarUint8Array(decoder),
|
decoding.readVarUint8Array(decoder),
|
||||||
conn
|
conn,
|
||||||
);
|
);
|
||||||
// console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`)
|
// console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`)
|
||||||
break;
|
break;
|
||||||
|
@ -83,7 +98,12 @@ export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHandler = (update: Uint8Array, _origin: any, doc: WSSharedDoc, _tr: any) => {
|
const updateHandler = (
|
||||||
|
update: Uint8Array,
|
||||||
|
_origin: any,
|
||||||
|
doc: WSSharedDoc,
|
||||||
|
_tr: any,
|
||||||
|
) => {
|
||||||
const encoder = encoding.createEncoder();
|
const encoder = encoding.createEncoder();
|
||||||
encoding.writeVarUint(encoder, YMessageType.Sync);
|
encoding.writeVarUint(encoder, YMessageType.Sync);
|
||||||
writeUpdate(encoder, update);
|
writeUpdate(encoder, update);
|
||||||
|
@ -91,7 +111,8 @@ const updateHandler = (update: Uint8Array, _origin: any, doc: WSSharedDoc, _tr:
|
||||||
doc.conns.forEach((_, conn) => send(doc, conn, message));
|
doc.conns.forEach((_, conn) => send(doc, conn, message));
|
||||||
};
|
};
|
||||||
|
|
||||||
let contentInitializor: (ydoc: Y.Doc) => Promise<void> = (_ydoc) => Promise.resolve();
|
let contentInitializor: (ydoc: Y.Doc) => Promise<void> = (_ydoc) =>
|
||||||
|
Promise.resolve();
|
||||||
export const setContentInitializor = (f: (ydoc: Y.Doc) => Promise<void>) => {
|
export const setContentInitializor = (f: (ydoc: Y.Doc) => Promise<void>) => {
|
||||||
contentInitializor = f;
|
contentInitializor = f;
|
||||||
};
|
};
|
||||||
|
@ -110,22 +131,29 @@ export class WSSharedDoc extends Y.Doc {
|
||||||
this.awareness = new Awareness(this);
|
this.awareness = new Awareness(this);
|
||||||
this.awareness.setLocalState(null);
|
this.awareness.setLocalState(null);
|
||||||
|
|
||||||
const awarenessUpdateHandler = ({
|
const awarenessUpdateHandler = (
|
||||||
|
{
|
||||||
added,
|
added,
|
||||||
updated,
|
updated,
|
||||||
removed
|
removed,
|
||||||
}: {
|
}: {
|
||||||
added: number[],
|
added: number[];
|
||||||
updated: number[],
|
updated: number[];
|
||||||
removed: number[]
|
removed: number[];
|
||||||
}, conn: WebSocket) => {
|
},
|
||||||
|
conn: WebSocket,
|
||||||
|
) => {
|
||||||
const changedClients = added.concat(updated, removed);
|
const changedClients = added.concat(updated, removed);
|
||||||
if (changedClients.length === 0) return
|
if (changedClients.length === 0) return;
|
||||||
if (conn !== null) {
|
if (conn !== null) {
|
||||||
const connControlledIDs = this.conns.get(conn) as Set<number>;
|
const connControlledIDs = this.conns.get(conn) as Set<number>;
|
||||||
if (connControlledIDs !== undefined) {
|
if (connControlledIDs !== undefined) {
|
||||||
added.forEach(clientID => { connControlledIDs.add(clientID); });
|
added.forEach((clientID) => {
|
||||||
removed.forEach(clientID => { connControlledIDs.delete(clientID); });
|
connControlledIDs.add(clientID);
|
||||||
|
});
|
||||||
|
removed.forEach((clientID) => {
|
||||||
|
connControlledIDs.delete(clientID);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +161,7 @@ export class WSSharedDoc extends Y.Doc {
|
||||||
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
||||||
encoding.writeVarUint8Array(
|
encoding.writeVarUint8Array(
|
||||||
encoder,
|
encoder,
|
||||||
encodeAwarenessUpdate(this.awareness, changedClients)
|
encodeAwarenessUpdate(this.awareness, changedClients),
|
||||||
);
|
);
|
||||||
const buff = encoding.toUint8Array(encoder);
|
const buff = encoding.toUint8Array(encoder);
|
||||||
|
|
||||||
|
@ -146,11 +174,12 @@ export class WSSharedDoc extends Y.Doc {
|
||||||
this.on('update', updateHandler as any);
|
this.on('update', updateHandler as any);
|
||||||
|
|
||||||
if (isCallbackSet) {
|
if (isCallbackSet) {
|
||||||
this.on('update', debounce(
|
this.on(
|
||||||
callbackHandler as any,
|
'update',
|
||||||
CALLBACK_DEBOUNCE_WAIT,
|
debounce(callbackHandler as any, CALLBACK_DEBOUNCE_WAIT, {
|
||||||
{ maxWait: CALLBACK_DEBOUNCE_MAXWAIT }
|
maxWait: CALLBACK_DEBOUNCE_MAXWAIT,
|
||||||
) as any);
|
}) as any,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.whenInitialized = contentInitializor(this);
|
this.whenInitialized = contentInitializor(this);
|
||||||
|
|
|
@ -1,25 +1,36 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from '@nestjs/common';
|
||||||
import { WebSocketType, WSClient } from "../types";
|
import { WebSocketType, WSClient } from '../types';
|
||||||
import { BaseWebSocketServer } from "../base/base-websocket-server";
|
import { BaseWebSocketServer } from '../base/base-websocket-server';
|
||||||
import { encoding } from "lib0";
|
import { encoding } from 'lib0';
|
||||||
import { YMessageType, writeSyncStep1, encodeAwarenessUpdate } from "@nice/common";
|
import {
|
||||||
import { getYDoc, closeConn, WSSharedDoc, messageListener, send } from "./ws-shared-doc";
|
YMessageType,
|
||||||
|
writeSyncStep1,
|
||||||
|
encodeAwarenessUpdate,
|
||||||
|
} from '@nice/common';
|
||||||
|
import {
|
||||||
|
getYDoc,
|
||||||
|
closeConn,
|
||||||
|
WSSharedDoc,
|
||||||
|
messageListener,
|
||||||
|
send,
|
||||||
|
} from './ws-shared-doc';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class YjsServer extends BaseWebSocketServer {
|
export class YjsServer extends BaseWebSocketServer {
|
||||||
public get serverType(): WebSocketType {
|
public get serverType(): WebSocketType {
|
||||||
return WebSocketType.YJS;
|
return WebSocketType.YJS;
|
||||||
}
|
}
|
||||||
public override handleConnection(
|
public override handleConnection(connection: WSClient): void {
|
||||||
connection: WSClient
|
super.handleConnection(connection);
|
||||||
): void {
|
|
||||||
super.handleConnection(connection)
|
|
||||||
try {
|
try {
|
||||||
connection.binaryType = 'arraybuffer';
|
connection.binaryType = 'arraybuffer';
|
||||||
const doc = this.initializeDocument(connection, connection.roomId, true);
|
const doc = this.initializeDocument(connection, connection.roomId, true);
|
||||||
this.setupConnectionHandlers(connection, doc);
|
this.setupConnectionHandlers(connection, doc);
|
||||||
this.sendInitialSync(connection, doc);
|
this.sendInitialSync(connection, doc);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Error in handleNewConnection: ${error.message}`, error.stack);
|
this.logger.error(
|
||||||
|
`Error in handleNewConnection: ${error.message}`,
|
||||||
|
error.stack,
|
||||||
|
);
|
||||||
connection.close();
|
connection.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +42,10 @@ export class YjsServer extends BaseWebSocketServer {
|
||||||
return doc;
|
return doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupConnectionHandlers(connection: WSClient, doc: WSSharedDoc): void {
|
private setupConnectionHandlers(
|
||||||
|
connection: WSClient,
|
||||||
|
doc: WSSharedDoc,
|
||||||
|
): void {
|
||||||
connection.on('message', (message: ArrayBuffer) => {
|
connection.on('message', (message: ArrayBuffer) => {
|
||||||
this.handleMessage(connection, doc, message);
|
this.handleMessage(connection, doc, message);
|
||||||
});
|
});
|
||||||
|
@ -39,9 +53,14 @@ export class YjsServer extends BaseWebSocketServer {
|
||||||
this.handleClose(doc, connection);
|
this.handleClose(doc, connection);
|
||||||
});
|
});
|
||||||
connection.on('error', (error) => {
|
connection.on('error', (error) => {
|
||||||
this.logger.error(`WebSocket error for doc ${doc.name}: ${error.message}`, error.stack);
|
this.logger.error(
|
||||||
|
`WebSocket error for doc ${doc.name}: ${error.message}`,
|
||||||
|
error.stack,
|
||||||
|
);
|
||||||
closeConn(doc, connection);
|
closeConn(doc, connection);
|
||||||
this.logger.warn(`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`);
|
this.logger.warn(
|
||||||
|
`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,14 +68,24 @@ export class YjsServer extends BaseWebSocketServer {
|
||||||
try {
|
try {
|
||||||
closeConn(doc, connection);
|
closeConn(doc, connection);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Error closing connection: ${error.message}`, error.stack);
|
this.logger.error(
|
||||||
|
`Error closing connection: ${error.message}`,
|
||||||
|
error.stack,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private handleMessage(connection: WSClient, doc: WSSharedDoc, message: ArrayBuffer): void {
|
private handleMessage(
|
||||||
|
connection: WSClient,
|
||||||
|
doc: WSSharedDoc,
|
||||||
|
message: ArrayBuffer,
|
||||||
|
): void {
|
||||||
try {
|
try {
|
||||||
messageListener(connection, doc, new Uint8Array(message));
|
messageListener(connection, doc, new Uint8Array(message));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Error handling message: ${error.message}`, error.stack);
|
this.logger.error(
|
||||||
|
`Error handling message: ${error.message}`,
|
||||||
|
error.stack,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private sendInitialSync(connection: WSClient, doc: any): void {
|
private sendInitialSync(connection: WSClient, doc: any): void {
|
||||||
|
@ -77,7 +106,10 @@ export class YjsServer extends BaseWebSocketServer {
|
||||||
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
||||||
encoding.writeVarUint8Array(
|
encoding.writeVarUint8Array(
|
||||||
encoder,
|
encoder,
|
||||||
encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys()))
|
encodeAwarenessUpdate(
|
||||||
|
doc.awareness,
|
||||||
|
Array.from(awarenessStates.keys()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
send(doc, connection, encoding.toUint8Array(encoder));
|
send(doc, connection, encoding.toUint8Array(encoder));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { RealtimeServer } from './realtime.server';
|
import { RealtimeServer } from './realtime.server';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [ RealtimeServer],
|
providers: [RealtimeServer],
|
||||||
exports: [ RealtimeServer]
|
exports: [RealtimeServer],
|
||||||
})
|
})
|
||||||
export class RealTimeModule { }
|
export class RealTimeModule {}
|
||||||
|
|
|
@ -1,23 +1,35 @@
|
||||||
import { Injectable, OnModuleInit } from "@nestjs/common";
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import { WebSocketType } from "../types";
|
import { WebSocketType } from '../types';
|
||||||
import { BaseWebSocketServer } from "../base/base-websocket-server";
|
import { BaseWebSocketServer } from '../base/base-websocket-server';
|
||||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||||
import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nice/common";
|
import {
|
||||||
|
ObjectType,
|
||||||
|
SocketMsgType,
|
||||||
|
MessageDto,
|
||||||
|
PostDto,
|
||||||
|
PostType,
|
||||||
|
} from '@nice/common';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit {
|
export class RealtimeServer
|
||||||
|
extends BaseWebSocketServer
|
||||||
|
implements OnModuleInit
|
||||||
|
{
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
EventBus.on("dataChanged", ({ data, type, operation }) => {
|
EventBus.on('dataChanged', ({ data, type, operation }) => {
|
||||||
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
||||||
const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id)
|
const receiverIds = (data as Partial<MessageDto>).receivers.map(
|
||||||
this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } })
|
(receiver) => receiver.id,
|
||||||
|
);
|
||||||
|
this.sendToUsers(receiverIds, {
|
||||||
|
type: SocketMsgType.NOTIFY,
|
||||||
|
payload: { objectType: ObjectType.MESSAGE },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === ObjectType.POST) {
|
if (type === ObjectType.POST) {
|
||||||
const post = data as Partial<PostDto>
|
const post = data as Partial<PostDto>;
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
public get serverType(): WebSocketType {
|
public get serverType(): WebSocketType {
|
||||||
return WebSocketType.REALTIME;
|
return WebSocketType.REALTIME;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { WebSocketServer, WebSocket } from "ws";
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
|
|
||||||
// 类型定义
|
// 类型定义
|
||||||
export enum WebSocketType {
|
export enum WebSocketType {
|
||||||
YJS = "yjs",
|
YJS = 'yjs',
|
||||||
REALTIME = "realtime"
|
REALTIME = 'realtime',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebSocketServerConfig {
|
export interface WebSocketServerConfig {
|
||||||
path?: string;
|
path?: string;
|
||||||
pingInterval?: number;
|
pingInterval?: number;
|
||||||
pingTimeout?: number;
|
pingTimeout?: number;
|
||||||
debug?: boolean
|
debug?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServerInstance {
|
export interface ServerInstance {
|
||||||
|
@ -23,7 +23,7 @@ export interface ServerInstance {
|
||||||
export interface WSClient extends WebSocket {
|
export interface WSClient extends WebSocket {
|
||||||
isAlive?: boolean;
|
isAlive?: boolean;
|
||||||
type?: WebSocketType;
|
type?: WebSocketType;
|
||||||
userId?: string
|
userId?: string;
|
||||||
origin?: string
|
origin?: string;
|
||||||
roomId?: string
|
roomId?: string;
|
||||||
}
|
}
|
|
@ -8,4 +8,4 @@ import { CollaborationModule } from './collaboration/collaboration.module';
|
||||||
providers: [WebSocketService],
|
providers: [WebSocketService],
|
||||||
exports: [WebSocketService],
|
exports: [WebSocketService],
|
||||||
})
|
})
|
||||||
export class WebSocketModule { }
|
export class WebSocketModule {}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { Server } from "http";
|
import { Server } from 'http';
|
||||||
import { WSClient } from "./types";
|
import { WSClient } from './types';
|
||||||
import { RealtimeServer } from "./realtime/realtime.server";
|
import { RealtimeServer } from './realtime/realtime.server';
|
||||||
import { YjsServer } from "./collaboration/yjs.server";
|
import { YjsServer } from './collaboration/yjs.server';
|
||||||
import { BaseWebSocketServer } from "./base/base-websocket-server";
|
import { BaseWebSocketServer } from './base/base-websocket-server';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebSocketService {
|
export class WebSocketService {
|
||||||
|
@ -11,14 +11,14 @@ export class WebSocketService {
|
||||||
private readonly servers: BaseWebSocketServer[] = [];
|
private readonly servers: BaseWebSocketServer[] = [];
|
||||||
constructor(
|
constructor(
|
||||||
private realTimeServer: RealtimeServer,
|
private realTimeServer: RealtimeServer,
|
||||||
private yjsServer: YjsServer
|
private yjsServer: YjsServer,
|
||||||
) {
|
) {
|
||||||
this.servers.push(this.realTimeServer)
|
this.servers.push(this.realTimeServer);
|
||||||
this.servers.push(this.yjsServer)
|
this.servers.push(this.yjsServer);
|
||||||
}
|
}
|
||||||
public async initialize(httpServer: Server): Promise<void> {
|
public async initialize(httpServer: Server): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await Promise.all(this.servers.map(server => server.start()));
|
await Promise.all(this.servers.map((server) => server.start()));
|
||||||
this.setupUpgradeHandler(httpServer);
|
this.setupUpgradeHandler(httpServer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to initialize:', error);
|
this.logger.error('Failed to initialize:', error);
|
||||||
|
@ -36,7 +36,7 @@ export class WebSocketService {
|
||||||
const urlParams = new URLSearchParams(url.search);
|
const urlParams = new URLSearchParams(url.search);
|
||||||
const roomId = urlParams.get('roomId');
|
const roomId = urlParams.get('roomId');
|
||||||
const userId = urlParams.get('userId');
|
const userId = urlParams.get('userId');
|
||||||
const server = this.servers.find(server => {
|
const server = this.servers.find((server) => {
|
||||||
const serverPathClean = server.serverPath.replace(/\/$/, '');
|
const serverPathClean = server.serverPath.replace(/\/$/, '');
|
||||||
const pathnameClean = pathname.replace(/\/$/, '');
|
const pathnameClean = pathname.replace(/\/$/, '');
|
||||||
return serverPathClean === pathnameClean;
|
return serverPathClean === pathnameClean;
|
||||||
|
@ -48,8 +48,8 @@ export class WebSocketService {
|
||||||
|
|
||||||
server.wss!.handleUpgrade(request, socket, head, (ws: WSClient) => {
|
server.wss!.handleUpgrade(request, socket, head, (ws: WSClient) => {
|
||||||
ws.userId = userId;
|
ws.userId = userId;
|
||||||
ws.origin = request.url
|
ws.origin = request.url;
|
||||||
ws.roomId = roomId
|
ws.roomId = roomId;
|
||||||
server.wss!.emit('connection', ws, request);
|
server.wss!.emit('connection', ws, request);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -52,8 +52,8 @@ export class GenDevService {
|
||||||
await this.generateStaffs(4);
|
await this.generateStaffs(4);
|
||||||
//await this.generateTerms(2, 6);
|
//await this.generateTerms(2, 6);
|
||||||
//await this.generateCourses(8);
|
//await this.generateCourses(8);
|
||||||
await this.generateTrainContent(2,6)
|
await this.generateTrainContent(2, 6);
|
||||||
await this.generateTrainSituations()
|
await this.generateTrainSituations();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,9 @@ export class GenDevService {
|
||||||
this.domains.forEach((domain) => {
|
this.domains.forEach((domain) => {
|
||||||
this.domainDepts[domain.id] = this.getAllChildDepartments(domain.id);
|
this.domainDepts[domain.id] = this.getAllChildDepartments(domain.id);
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Domain: ${domain.name} has ${this.domainDepts[domain.id].length} child departments`,
|
`Domain: ${domain.name} has ${
|
||||||
|
this.domainDepts[domain.id].length
|
||||||
|
} child departments`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.logger.log(`Completed: Generated ${this.depts.length} departments.`);
|
this.logger.log(`Completed: Generated ${this.depts.length} departments.`);
|
||||||
|
@ -104,7 +106,9 @@ export class GenDevService {
|
||||||
if (currentDepth > maxDepth) return;
|
if (currentDepth > maxDepth) return;
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const deptName = `${parentId?.slice(0, 6) || '根'}公司${currentDepth}-${i}`;
|
const deptName = `${
|
||||||
|
parentId?.slice(0, 6) || '根'
|
||||||
|
}公司${currentDepth}-${i}`;
|
||||||
const newDept = await this.createDepartment(
|
const newDept = await this.createDepartment(
|
||||||
deptName,
|
deptName,
|
||||||
parentId,
|
parentId,
|
||||||
|
@ -190,7 +194,9 @@ export class GenDevService {
|
||||||
});
|
});
|
||||||
for (const cate of cates) {
|
for (const cate of cates) {
|
||||||
for (let i = 0; i < countPerCate; i++) {
|
for (let i = 0; i < countPerCate; i++) {
|
||||||
const randomTitle = `${titleList[Math.floor(Math.random() * titleList.length)]} ${Math.random().toString(36).substring(7)}`;
|
const randomTitle = `${
|
||||||
|
titleList[Math.floor(Math.random() * titleList.length)]
|
||||||
|
} ${Math.random().toString(36).substring(7)}`;
|
||||||
const randomLevelId =
|
const randomLevelId =
|
||||||
levels[Math.floor(Math.random() * levels.length)].id;
|
levels[Math.floor(Math.random() * levels.length)].id;
|
||||||
const randomDeptId =
|
const randomDeptId =
|
||||||
|
@ -224,7 +230,9 @@ export class GenDevService {
|
||||||
this.deptStaffRecord[dept.id] = [];
|
this.deptStaffRecord[dept.id] = [];
|
||||||
}
|
}
|
||||||
for (let i = 0; i < countPerDept; i++) {
|
for (let i = 0; i < countPerDept; i++) {
|
||||||
const username = `${dept.name}-S${staffsGenerated.toString().padStart(4, '0')}`;
|
const username = `${dept.name}-S${staffsGenerated
|
||||||
|
.toString()
|
||||||
|
.padStart(4, '0')}`;
|
||||||
const staff = await this.staffService.create({
|
const staff = await this.staffService.create({
|
||||||
data: {
|
data: {
|
||||||
showname: username,
|
showname: username,
|
||||||
|
@ -328,7 +336,9 @@ export class GenDevService {
|
||||||
) => {
|
) => {
|
||||||
if (currentDepth > depth) return;
|
if (currentDepth > depth) return;
|
||||||
for (let i = 0; i < nodesPerLevel; i++) {
|
for (let i = 0; i < nodesPerLevel; i++) {
|
||||||
const name = `${taxonomySlug}-${domain?.name || 'public'}-${currentDepth}-${counter++} `;
|
const name = `${taxonomySlug}-${
|
||||||
|
domain?.name || 'public'
|
||||||
|
}-${currentDepth}-${counter++} `;
|
||||||
const newTerm = await this.termService.create({
|
const newTerm = await this.termService.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
|
@ -347,106 +357,113 @@ export class GenDevService {
|
||||||
}
|
}
|
||||||
// 生成培训内容
|
// 生成培训内容
|
||||||
private async createTrainContent(
|
private async createTrainContent(
|
||||||
type:string,
|
type: string,
|
||||||
title:string,
|
title: string,
|
||||||
parentId:string|null
|
parentId: string | null,
|
||||||
){
|
) {
|
||||||
const trainContent = await db.trainContent.create({
|
const trainContent = await db.trainContent.create({
|
||||||
data: {
|
data: {
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
parentId
|
parentId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return trainContent;
|
return trainContent;
|
||||||
}
|
}
|
||||||
// 生成培训内容
|
// 生成培训内容
|
||||||
private async generateTrainContent(depth:number=3,count:number=6){
|
private async generateTrainContent(depth: number = 3, count: number = 6) {
|
||||||
if(this.counts.trainContentCount !== 0) return;
|
if (this.counts.trainContentCount !== 0) return;
|
||||||
const totalTrainContent = this.calculateTotalTrainContent(depth,count)
|
const totalTrainContent = this.calculateTotalTrainContent(depth, count);
|
||||||
this.logger.log("Start generating train content...")
|
this.logger.log('Start generating train content...');
|
||||||
await this.generateSubTrainContent(null,1,depth,count,totalTrainContent)
|
await this.generateSubTrainContent(
|
||||||
this.trainContents = await db.trainContent.findMany()
|
null,
|
||||||
this.logger.log(`Completed: Generated ${this.trainContents.length} departments.`);
|
1,
|
||||||
|
depth,
|
||||||
|
count,
|
||||||
|
totalTrainContent,
|
||||||
|
);
|
||||||
|
this.trainContents = await db.trainContent.findMany();
|
||||||
|
this.logger.log(
|
||||||
|
`Completed: Generated ${this.trainContents.length} departments.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// 生成培训内容子内容
|
// 生成培训内容子内容
|
||||||
private async generateSubTrainContent(
|
private async generateSubTrainContent(
|
||||||
parentId: string | null,
|
parentId: string | null,
|
||||||
currentDepth:number,
|
currentDepth: number,
|
||||||
maxDepth:number,
|
maxDepth: number,
|
||||||
count:number,
|
count: number,
|
||||||
total:number,
|
total: number,
|
||||||
){
|
) {
|
||||||
if(currentDepth > maxDepth) return;
|
if (currentDepth > maxDepth) return;
|
||||||
const contentType = [TrainContentType.SUBJECTS,TrainContentType.COURSE]
|
const contentType = [TrainContentType.SUBJECTS, TrainContentType.COURSE];
|
||||||
for(let i = 0 ; i < count ; i++){
|
for (let i = 0; i < count; i++) {
|
||||||
const trainContentTitle = `${parentId?.slice(0,6) || '根'}公司${currentDepth}-${i}`
|
const trainContentTitle = `${
|
||||||
|
parentId?.slice(0, 6) || '根'
|
||||||
|
}公司${currentDepth}-${i}`;
|
||||||
const newTrainContent = await this.createTrainContent(
|
const newTrainContent = await this.createTrainContent(
|
||||||
contentType[currentDepth-1],
|
contentType[currentDepth - 1],
|
||||||
trainContentTitle,
|
trainContentTitle,
|
||||||
parentId
|
parentId,
|
||||||
)
|
);
|
||||||
this.trainContentGeneratedCount++;
|
this.trainContentGeneratedCount++;
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Generated ${this.trainContentGeneratedCount}/${total} train contents`
|
`Generated ${this.trainContentGeneratedCount}/${total} train contents`,
|
||||||
)
|
);
|
||||||
await this.generateSubTrainContent(
|
await this.generateSubTrainContent(
|
||||||
newTrainContent.id,
|
newTrainContent.id,
|
||||||
currentDepth+1,
|
currentDepth + 1,
|
||||||
maxDepth,
|
maxDepth,
|
||||||
count,
|
count,
|
||||||
total
|
total,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private calculateTotalTrainContent(depth:number,count:number):number{
|
private calculateTotalTrainContent(depth: number, count: number): number {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
for(let i = 1 ; i<=depth;i++){
|
for (let i = 1; i <= depth; i++) {
|
||||||
total += Math.pow(count,i);
|
total += Math.pow(count, i);
|
||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async createTrainSituation(staffId: string, trainContentId: string) {
|
||||||
private async createTrainSituation(staffId:string,trainContentId:string){
|
|
||||||
const trainSituation = await db.trainSituation.create({
|
const trainSituation = await db.trainSituation.create({
|
||||||
data:{
|
data: {
|
||||||
staffId,
|
staffId,
|
||||||
trainContentId,
|
trainContentId,
|
||||||
mustTrainTime:Math.floor(Math.random()*100),
|
mustTrainTime: Math.floor(Math.random() * 100),
|
||||||
alreadyTrainTime:Math.floor(Math.random()*100),
|
alreadyTrainTime: Math.floor(Math.random() * 100),
|
||||||
score:Math.floor(Math.random()*100),
|
score: Math.floor(Math.random() * 100),
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
return trainSituation
|
return trainSituation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateTrainSituations(probability: number = 0.1){
|
private async generateTrainSituations(probability: number = 0.1) {
|
||||||
this.logger.log("Start generating train situations...")
|
this.logger.log('Start generating train situations...');
|
||||||
const allTrainContents = await db.trainContent.findMany();
|
const allTrainContents = await db.trainContent.findMany();
|
||||||
// 这里相当于两次遍历 找到没有parentID的即是
|
// 这里相当于两次遍历 找到没有parentID的即是
|
||||||
const leafNodes = allTrainContents.filter((item)=>item.parentId !== null)
|
const leafNodes = allTrainContents.filter((item) => item.parentId !== null);
|
||||||
console.log(leafNodes.length)
|
console.log(leafNodes.length);
|
||||||
const staffs = await db.staff.findMany()
|
const staffs = await db.staff.findMany();
|
||||||
|
|
||||||
let situationCount = 0
|
let situationCount = 0;
|
||||||
const totalPossibleSituations = leafNodes.length * staffs.length
|
const totalPossibleSituations = leafNodes.length * staffs.length;
|
||||||
|
|
||||||
for (const staff of staffs){
|
for (const staff of staffs) {
|
||||||
for(const leaf of leafNodes){
|
for (const leaf of leafNodes) {
|
||||||
if(Math.random() < probability){
|
if (Math.random() < probability) {
|
||||||
await this.createTrainSituation(staff.id,leaf.id)
|
await this.createTrainSituation(staff.id, leaf.id);
|
||||||
situationCount++
|
situationCount++;
|
||||||
if (situationCount % 100 === 0) {
|
if (situationCount % 100 === 0) {
|
||||||
this.logger.log(
|
this.logger.log(`Generated ${situationCount} train situations`);
|
||||||
`Generated ${situationCount} train situations`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.`
|
`Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,8 +9,15 @@ import { DepartmentModule } from '@server/models/department/department.module';
|
||||||
import { TermModule } from '@server/models/term/term.module';
|
import { TermModule } from '@server/models/term/term.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MinioModule, AuthModule, AppConfigModule, StaffModule, DepartmentModule, TermModule],
|
imports: [
|
||||||
|
MinioModule,
|
||||||
|
AuthModule,
|
||||||
|
AppConfigModule,
|
||||||
|
StaffModule,
|
||||||
|
DepartmentModule,
|
||||||
|
TermModule,
|
||||||
|
],
|
||||||
providers: [InitService, GenDevService],
|
providers: [InitService, GenDevService],
|
||||||
exports: [InitService]
|
exports: [InitService],
|
||||||
})
|
})
|
||||||
export class InitModule { }
|
export class InitModule {}
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class InitService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.logger.log(`Created new taxonomy: ${taxonomy.name}`);
|
this.logger.log(`Created new taxonomy: ${taxonomy.name}`);
|
||||||
} else if(process.env.NODE_ENV === 'development'){
|
} else if (process.env.NODE_ENV === 'development') {
|
||||||
// Check for differences and update if necessary
|
// Check for differences and update if necessary
|
||||||
const differences = Object.keys(taxonomy).filter(
|
const differences = Object.keys(taxonomy).filter(
|
||||||
(key) => taxonomy[key] !== existingTaxonomy[key],
|
(key) => taxonomy[key] !== existingTaxonomy[key],
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface DevDataCounts {
|
||||||
export async function getCounts(): Promise<DevDataCounts> {
|
export async function getCounts(): Promise<DevDataCounts> {
|
||||||
const counts = {
|
const counts = {
|
||||||
deptCount: await db.department.count(),
|
deptCount: await db.department.count(),
|
||||||
trainContentCount:await db.trainContent.count(),
|
trainContentCount: await db.trainContent.count(),
|
||||||
staffCount: await db.staff.count(),
|
staffCount: await db.staff.count(),
|
||||||
termCount: await db.term.count(),
|
termCount: await db.term.count(),
|
||||||
courseCount: await db.post.count({
|
courseCount: await db.post.count({
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { ReminderService } from './reminder.service';
|
||||||
import { MessageModule } from '@server/models/message/message.module';
|
import { MessageModule } from '@server/models/message/message.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ MessageModule],
|
imports: [MessageModule],
|
||||||
providers: [ReminderService],
|
providers: [ReminderService],
|
||||||
exports: [ReminderService]
|
exports: [ReminderService],
|
||||||
})
|
})
|
||||||
export class ReminderModule { }
|
export class ReminderModule {}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export class ReminderService {
|
||||||
* 构造函数
|
* 构造函数
|
||||||
* @param messageService 消息服务实例
|
* @param messageService 消息服务实例
|
||||||
*/
|
*/
|
||||||
constructor(private readonly messageService: MessageService) { }
|
constructor(private readonly messageService: MessageService) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成提醒时间点
|
* 生成提醒时间点
|
||||||
|
@ -75,7 +75,5 @@ export class ReminderService {
|
||||||
*/
|
*/
|
||||||
async remindDeadline() {
|
async remindDeadline() {
|
||||||
this.logger.log('开始检查截止日期以发送提醒。');
|
this.logger.log('开始检查截止日期以发送提醒。');
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TasksService } from './tasks.service';
|
import { TasksService } from './tasks.service';
|
||||||
import { InitModule } from '@server/tasks/init/init.module';
|
import { InitModule } from '@server/tasks/init/init.module';
|
||||||
import { ReminderModule } from "@server/tasks/reminder/reminder.module"
|
import { ReminderModule } from '@server/tasks/reminder/reminder.module';
|
||||||
@Module({
|
@Module({
|
||||||
imports: [InitModule, ReminderModule],
|
imports: [InitModule, ReminderModule],
|
||||||
providers: [TasksService]
|
providers: [TasksService],
|
||||||
})
|
})
|
||||||
export class TasksModule { }
|
export class TasksModule {}
|
||||||
|
|
|
@ -11,8 +11,8 @@ export class TasksService implements OnModuleInit {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly schedulerRegistry: SchedulerRegistry,
|
private readonly schedulerRegistry: SchedulerRegistry,
|
||||||
private readonly initService: InitService,
|
private readonly initService: InitService,
|
||||||
private readonly reminderService: ReminderService
|
private readonly reminderService: ReminderService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
this.logger.log('Main node launch');
|
this.logger.log('Main node launch');
|
||||||
|
@ -30,11 +30,17 @@ export class TasksService implements OnModuleInit {
|
||||||
await this.reminderService.remindDeadline();
|
await this.reminderService.remindDeadline();
|
||||||
this.logger.log('Reminder successfully processed');
|
this.logger.log('Reminder successfully processed');
|
||||||
} catch (reminderErr) {
|
} catch (reminderErr) {
|
||||||
this.logger.error('Error occurred while processing reminder', reminderErr);
|
this.logger.error(
|
||||||
|
'Error occurred while processing reminder',
|
||||||
|
reminderErr,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.schedulerRegistry.addCronJob('remindDeadline', handleRemindJob as any);
|
this.schedulerRegistry.addCronJob(
|
||||||
|
'remindDeadline',
|
||||||
|
handleRemindJob as any,
|
||||||
|
);
|
||||||
this.logger.log('Start remind cron job');
|
this.logger.log('Start remind cron job');
|
||||||
handleRemindJob.start();
|
handleRemindJob.start();
|
||||||
} catch (cronJobErr) {
|
} catch (cronJobErr) {
|
||||||
|
|
|
@ -37,8 +37,8 @@ export class TrpcRouter {
|
||||||
private readonly visitor: VisitRouter,
|
private readonly visitor: VisitRouter,
|
||||||
private readonly resource: ResourceRouter,
|
private readonly resource: ResourceRouter,
|
||||||
private readonly trainContent: TrainContentRouter,
|
private readonly trainContent: TrainContentRouter,
|
||||||
private readonly trainSituation:TrainSituationRouter,
|
private readonly trainSituation: TrainSituationRouter,
|
||||||
private readonly dailyTrain:DailyTrainRouter,
|
private readonly dailyTrain: DailyTrainRouter,
|
||||||
private readonly systemLogRouter: SystemLogRouter,
|
private readonly systemLogRouter: SystemLogRouter,
|
||||||
) {}
|
) {}
|
||||||
getRouter() {
|
getRouter() {
|
||||||
|
@ -57,9 +57,9 @@ export class TrpcRouter {
|
||||||
app_config: this.app_config.router,
|
app_config: this.app_config.router,
|
||||||
visitor: this.visitor.router,
|
visitor: this.visitor.router,
|
||||||
resource: this.resource.router,
|
resource: this.resource.router,
|
||||||
trainContent:this.trainContent.router,
|
trainContent: this.trainContent.router,
|
||||||
trainSituation:this.trainSituation.router,
|
trainSituation: this.trainSituation.router,
|
||||||
dailyTrain:this.dailyTrain.router,
|
dailyTrain: this.dailyTrain.router,
|
||||||
systemLog: this.systemLogRouter.router,
|
systemLog: this.systemLogRouter.router,
|
||||||
});
|
});
|
||||||
wss: WebSocketServer = undefined;
|
wss: WebSocketServer = undefined;
|
||||||
|
|
|
@ -18,8 +18,9 @@ export class TrpcService {
|
||||||
ip: string;
|
ip: string;
|
||||||
}> {
|
}> {
|
||||||
const token = opts.req.headers.authorization?.split(' ')[1];
|
const token = opts.req.headers.authorization?.split(' ')[1];
|
||||||
const staff =
|
const staff = await UserProfileService.instance.getUserProfileByToken(
|
||||||
await UserProfileService.instance.getUserProfileByToken(token);
|
token,
|
||||||
|
);
|
||||||
const ip = getClientIp(opts.req);
|
const ip = getClientIp(opts.req);
|
||||||
return {
|
return {
|
||||||
staff: staff.staff,
|
staff: staff.staff,
|
||||||
|
|
|
@ -131,5 +131,4 @@ export class TusService implements OnModuleInit {
|
||||||
// console.log(req)
|
// console.log(req)
|
||||||
return this.tusServer.handle(req, res);
|
return this.tusServer.handle(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,9 +69,9 @@ export class UploadController {
|
||||||
// 直接返回正确的数据结构
|
// 直接返回正确的数据结构
|
||||||
const response = {
|
const response = {
|
||||||
fileId: shareCode.fileId,
|
fileId: shareCode.fileId,
|
||||||
fileName:shareCode.fileName || 'downloaded_file',
|
fileName: shareCode.fileName || 'downloaded_file',
|
||||||
code: shareCode.code,
|
code: shareCode.code,
|
||||||
expiresAt: shareCode.expiresAt
|
expiresAt: shareCode.expiresAt,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('返回给前端的数据:', response); // 添加日志
|
console.log('返回给前端的数据:', response); // 添加日志
|
||||||
|
@ -118,9 +118,9 @@ export class UploadController {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
message: (error as Error).message || '生成分享码失败',
|
message: (error as Error).message || '生成分享码失败',
|
||||||
error: 'SHARE_CODE_GENERATION_FAILED'
|
error: 'SHARE_CODE_GENERATION_FAILED',
|
||||||
},
|
},
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ export class UploadController {
|
||||||
if (!data.fileId || !data.fileName) {
|
if (!data.fileId || !data.fileName) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{ message: '缺少必要参数' },
|
{ message: '缺少必要参数' },
|
||||||
HttpStatus.BAD_REQUEST
|
HttpStatus.BAD_REQUEST,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// 保存文件名
|
// 保存文件名
|
||||||
|
@ -147,9 +147,9 @@ export class UploadController {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
{
|
{
|
||||||
message: '保存文件名失败',
|
message: '保存文件名失败',
|
||||||
error: (error instanceof Error) ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
},
|
},
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -167,19 +167,18 @@ export class UploadController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取原始文件名
|
// 获取原始文件名
|
||||||
const fileName = await this.resourceService.getFileName(fileId) || 'downloaded-file';
|
const fileName =
|
||||||
|
(await this.resourceService.getFileName(fileId)) || 'downloaded-file';
|
||||||
|
|
||||||
// 设置响应头,包含原始文件名
|
// 设置响应头,包含原始文件名
|
||||||
res.setHeader(
|
res.setHeader(
|
||||||
'Content-Disposition',
|
'Content-Disposition',
|
||||||
`attachment; filename="${encodeURIComponent(fileName)}"`
|
`attachment; filename="${encodeURIComponent(fileName)}"`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 其他下载逻辑...
|
// 其他下载逻辑...
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 错误处理...
|
// 错误处理...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,6 @@ import { MinioService } from './minio.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [MinioService],
|
providers: [MinioService],
|
||||||
exports: [MinioService]
|
exports: [MinioService],
|
||||||
})
|
})
|
||||||
export class MinioModule {}
|
export class MinioModule {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { redis } from "./redis.service";
|
import { redis } from './redis.service';
|
||||||
|
|
||||||
export async function deleteByPattern(pattern: string) {
|
export async function deleteByPattern(pattern: string) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,36 +1,40 @@
|
||||||
import { createReadStream } from "fs";
|
import { createReadStream } from 'fs';
|
||||||
import { createInterface } from "readline";
|
import { createInterface } from 'readline';
|
||||||
|
|
||||||
import { db } from '@nice/common';
|
import { db } from '@nice/common';
|
||||||
import * as tus from "tus-js-client";
|
import * as tus from 'tus-js-client';
|
||||||
import ExcelJS from 'exceljs';
|
import ExcelJS from 'exceljs';
|
||||||
|
|
||||||
export function truncateStringByByte(str, maxBytes) {
|
export function truncateStringByByte(str, maxBytes) {
|
||||||
let byteCount = 0;
|
let byteCount = 0;
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < str.length && byteCount + new TextEncoder().encode(str[index]).length <= maxBytes) {
|
while (
|
||||||
|
index < str.length &&
|
||||||
|
byteCount + new TextEncoder().encode(str[index]).length <= maxBytes
|
||||||
|
) {
|
||||||
byteCount += new TextEncoder().encode(str[index]).length;
|
byteCount += new TextEncoder().encode(str[index]).length;
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
return str.substring(0, index) + (index < str.length ? "..." : "");
|
return str.substring(0, index) + (index < str.length ? '...' : '');
|
||||||
}
|
}
|
||||||
export async function loadPoliciesFromCSV(filePath: string) {
|
export async function loadPoliciesFromCSV(filePath: string) {
|
||||||
const policies = {
|
const policies = {
|
||||||
p: [],
|
p: [],
|
||||||
g: []
|
g: [],
|
||||||
};
|
};
|
||||||
const stream = createReadStream(filePath);
|
const stream = createReadStream(filePath);
|
||||||
const rl = createInterface({
|
const rl = createInterface({
|
||||||
input: stream,
|
input: stream,
|
||||||
crlfDelay: Infinity
|
crlfDelay: Infinity,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Updated regex to handle commas inside parentheses as part of a single field
|
// Updated regex to handle commas inside parentheses as part of a single field
|
||||||
const regex = /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g;
|
const regex =
|
||||||
|
/(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g;
|
||||||
|
|
||||||
for await (const line of rl) {
|
for await (const line of rl) {
|
||||||
// Ignore empty lines and comments
|
// Ignore empty lines and comments
|
||||||
if (line.trim() && !line.startsWith("#")) {
|
if (line.trim() && !line.startsWith('#')) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) !== null) {
|
while ((match = regex.exec(line)) !== null) {
|
||||||
|
@ -61,10 +65,10 @@ export function uploadFile(blob: any, fileName: string) {
|
||||||
metadata: {
|
metadata: {
|
||||||
filename: fileName,
|
filename: fileName,
|
||||||
filetype:
|
filetype:
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error("Failed because: " + error);
|
console.error('Failed because: ' + error);
|
||||||
reject(error); // 错误时,我们要拒绝 promise
|
reject(error); // 错误时,我们要拒绝 promise
|
||||||
},
|
},
|
||||||
onProgress: (bytesUploaded, bytesTotal) => {
|
onProgress: (bytesUploaded, bytesTotal) => {
|
||||||
|
@ -80,7 +84,6 @@ export function uploadFile(blob: any, fileName: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TreeNode {
|
class TreeNode {
|
||||||
value: string;
|
value: string;
|
||||||
children: TreeNode[];
|
children: TreeNode[];
|
||||||
|
@ -91,14 +94,12 @@ class TreeNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
addChild(childValue: string): TreeNode {
|
addChild(childValue: string): TreeNode {
|
||||||
let newChild = undefined
|
let newChild = undefined;
|
||||||
if (this.children.findIndex(child => child.value === childValue) === -1) {
|
if (this.children.findIndex((child) => child.value === childValue) === -1) {
|
||||||
newChild = new TreeNode(childValue);
|
newChild = new TreeNode(childValue);
|
||||||
this.children.push(newChild)
|
this.children.push(newChild);
|
||||||
|
|
||||||
}
|
}
|
||||||
return this.children.find(child => child.value === childValue)
|
return this.children.find((child) => child.value === childValue);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function buildTree(data: string[][]): TreeNode {
|
function buildTree(data: string[][]): TreeNode {
|
||||||
|
@ -111,12 +112,9 @@ function buildTree(data: string[][]): TreeNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return root;
|
return root;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
export function printTree(node: TreeNode, level: number = 0): void {
|
export function printTree(node: TreeNode, level: number = 0): void {
|
||||||
const indent = ' '.repeat(level);
|
const indent = ' '.repeat(level);
|
||||||
|
@ -133,9 +131,12 @@ export async function generateTreeFromFile(file: Buffer): Promise<TreeNode> {
|
||||||
const data: string[][] = [];
|
const data: string[][] = [];
|
||||||
|
|
||||||
worksheet.eachRow((row, rowNumber) => {
|
worksheet.eachRow((row, rowNumber) => {
|
||||||
if (rowNumber > 1) { // Skip header row if any
|
if (rowNumber > 1) {
|
||||||
const rowData: string[] = (row.values as string[]).slice(2).map(cell => (cell || '').toString());
|
// Skip header row if any
|
||||||
data.push(rowData.map(value => value.trim()));
|
const rowData: string[] = (row.values as string[])
|
||||||
|
.slice(2)
|
||||||
|
.map((cell) => (cell || '').toString());
|
||||||
|
data.push(rowData.map((value) => value.trim()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Fill forward values
|
// Fill forward values
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { PipeTransform, BadRequestException } from '@nestjs/common';
|
||||||
import { ZodSchema } from 'zod';
|
import { ZodSchema } from 'zod';
|
||||||
|
|
||||||
export class ZodValidationPipe implements PipeTransform {
|
export class ZodValidationPipe implements PipeTransform {
|
||||||
constructor(private schema: ZodSchema) { }
|
constructor(private schema: ZodSchema) {}
|
||||||
|
|
||||||
transform(value: unknown) {
|
transform(value: unknown) {
|
||||||
try {
|
try {
|
||||||
|
@ -11,7 +11,7 @@ export class ZodValidationPipe implements PipeTransform {
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
throw new BadRequestException('Validation failed', {
|
throw new BadRequestException('Validation failed', {
|
||||||
cause: error,
|
cause: error,
|
||||||
description: error.errors
|
description: error.errors,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { api } from '@nice/client';
|
||||||
|
import { Pagination } from 'antd';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
// 定义 TrainSituation 接口
|
||||||
|
interface TrainSituation {
|
||||||
|
id: string;
|
||||||
|
staffId: string;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义 Staff 接口
|
||||||
|
interface Staff {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
absent: boolean;
|
||||||
|
// trainSituations: TrainSituation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PaginatedResponse {
|
||||||
|
items: Staff[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TestPage: React.FC = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const pageSize = 10;
|
||||||
|
|
||||||
|
// 使用 findManyWithPagination 替换原来的两个查询
|
||||||
|
const { data, isLoading } = api.staff.findManyWithPagination.useQuery<PaginatedResponse>({
|
||||||
|
page: currentPage,
|
||||||
|
pageSize: pageSize,
|
||||||
|
where: { deletedAt: null },
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
absent: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
// data 中包含了分页数据和总记录数
|
||||||
|
const staffs = (data?.items || []) as Staff[];
|
||||||
|
const totalCount = data?.total || 0;
|
||||||
|
|
||||||
|
// 分页处理函数
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<h1 className="text-2xl font-bold mb-4 text-center">培训情况记录</h1>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full bg-white shadow-md rounded-lg">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gray-100 border-b">
|
||||||
|
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">培训记录ID</th>
|
||||||
|
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">员工ID</th>
|
||||||
|
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">在位</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{staffs.map((staff) => (
|
||||||
|
<tr key={staff.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
{staff.id}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
{staff.username}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
{staff.absent ? '在位' : '不在位'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
<Pagination
|
||||||
|
current={currentPage}
|
||||||
|
total={totalCount}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
showTotal={(total) => `共 ${total} 条记录`}
|
||||||
|
showSizeChanger={false}
|
||||||
|
showQuickJumper
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TestPage;
|
|
@ -1,23 +1,127 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Row, Col, Table, Badge, Empty, Tooltip } from "antd";
|
import { Row, Col, Table, Badge, Empty, Tooltip } from "antd";
|
||||||
import { TeamOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
import { TeamOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||||
import DashboardCard from "../../../components/presentation/dashboard-card";
|
import DashboardCard from "../../../components/presentation/dashboard-card";
|
||||||
import { theme } from "antd";
|
import { theme } from "antd";
|
||||||
|
import { EChartsOption } from "echarts-for-react";
|
||||||
|
|
||||||
interface DeptData {
|
interface DeptData {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DepartmentTableProps {
|
interface DepartmentTableProps {
|
||||||
filteredDeptData: DeptData[];
|
filteredDeptData: DeptData[];
|
||||||
staffs: any[] | undefined;
|
staffs: any[] | undefined;
|
||||||
}
|
}
|
||||||
|
// 图表配置函数
|
||||||
|
export const getPieChartOptions = (
|
||||||
|
deptData: DeptData[],
|
||||||
|
onEvents?: Record<string, (params: any) => void>
|
||||||
|
): EChartsOption => {
|
||||||
|
const top10Depts = deptData.slice(0, 10);
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "item",
|
||||||
|
formatter: "{a} <br/>{b}: {c}人 ({d}%)",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
orient: "vertical",
|
||||||
|
right: 10,
|
||||||
|
top: "center",
|
||||||
|
data: top10Depts.map((dept) => dept.name),
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "部门人数",
|
||||||
|
type: "pie",
|
||||||
|
radius: ["50%", "70%"],
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderColor: "#fff",
|
||||||
|
borderWidth: 2,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: "{b}: {c}人 ({d}%)",
|
||||||
|
position: "outside",
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
fontSize: "18",
|
||||||
|
fontWeight: "bold",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
data: top10Depts.map((dept) => ({
|
||||||
|
value: dept.count,
|
||||||
|
name: dept.name,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBarChartOptions = (deptData: DeptData[]): EChartsOption => {
|
||||||
|
const top10Depts = deptData.slice(0, 10);
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
axisPointer: {
|
||||||
|
type: "shadow",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: "3%",
|
||||||
|
right: "12%",
|
||||||
|
bottom: "3%",
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "value",
|
||||||
|
boundaryGap: [0, 0.01],
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: top10Depts.map((dept) => dept.name),
|
||||||
|
inverse: true,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: "人员数量",
|
||||||
|
type: "bar",
|
||||||
|
data: top10Depts.map((dept) => dept.count),
|
||||||
|
itemStyle: {
|
||||||
|
color: function (params) {
|
||||||
|
const colorList = [
|
||||||
|
"#91cc75",
|
||||||
|
"#5470c6",
|
||||||
|
"#ee6666",
|
||||||
|
"#73c0de",
|
||||||
|
"#3ba272",
|
||||||
|
"#fc8452",
|
||||||
|
"#9a60b4",
|
||||||
|
];
|
||||||
|
return colorList[params.dataIndex % colorList.length];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: "right",
|
||||||
|
formatter: "{c}人",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default function DepartmentTable({
|
export default function DepartmentTable({
|
||||||
filteredDeptData,
|
filteredDeptData,
|
||||||
staffs
|
staffs,
|
||||||
}: DepartmentTableProps): React.ReactElement {
|
}: DepartmentTableProps): React.ReactElement {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Row, Col, Statistic, Tooltip } from "antd";
|
import { Row, Col, Statistic, Tooltip } from "antd";
|
||||||
import { UserOutlined, TeamOutlined, IdcardOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
UserOutlined,
|
||||||
|
TeamOutlined,
|
||||||
|
IdcardOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import DashboardCard from "../../../components/presentation/dashboard-card";
|
import DashboardCard from "../../../components/presentation/dashboard-card";
|
||||||
import { theme } from "antd";
|
import { theme } from "antd";
|
||||||
|
|
||||||
interface PositionStats {
|
interface PositionStats {
|
||||||
total: number;
|
total: number;
|
||||||
distribution: any[];
|
distribution: any[];
|
||||||
topPosition: {name: string; count: number} | null;
|
topPosition: { name: string; count: number } | null;
|
||||||
vacantPositions: number;
|
vacantPositions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +25,7 @@ interface StatisticCardsProps {
|
||||||
export default function StatisticCards({
|
export default function StatisticCards({
|
||||||
staffs,
|
staffs,
|
||||||
departments,
|
departments,
|
||||||
positionStats
|
positionStats,
|
||||||
}: StatisticCardsProps): React.ReactElement {
|
}: StatisticCardsProps): React.ReactElement {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
import { EChartsOption } from "echarts-for-react";
|
|
||||||
|
|
||||||
interface DeptData {
|
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
// 图表配置函数
|
|
||||||
export const getPieChartOptions = (
|
|
||||||
deptData: DeptData[],
|
|
||||||
onEvents?: Record<string, (params: any) => void>
|
|
||||||
): EChartsOption => {
|
|
||||||
const top10Depts = deptData.slice(0, 10);
|
|
||||||
return {
|
|
||||||
tooltip: {
|
|
||||||
trigger: "item",
|
|
||||||
formatter: "{a} <br/>{b}: {c}人 ({d}%)",
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
orient: "vertical",
|
|
||||||
right: 10,
|
|
||||||
top: "center",
|
|
||||||
data: top10Depts.map((dept) => dept.name),
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "部门人数",
|
|
||||||
type: "pie",
|
|
||||||
radius: ["50%", "70%"],
|
|
||||||
avoidLabelOverlap: false,
|
|
||||||
itemStyle: {
|
|
||||||
borderRadius: 10,
|
|
||||||
borderColor: "#fff",
|
|
||||||
borderWidth: 2,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
formatter: "{b}: {c}人 ({d}%)",
|
|
||||||
position: "outside",
|
|
||||||
},
|
|
||||||
emphasis: {
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
fontSize: "18",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
labelLine: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
data: top10Depts.map((dept) => ({
|
|
||||||
value: dept.count,
|
|
||||||
name: dept.name,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getBarChartOptions = (deptData: DeptData[]): EChartsOption => {
|
|
||||||
const top10Depts = deptData.slice(0, 10);
|
|
||||||
return {
|
|
||||||
tooltip: {
|
|
||||||
trigger: "axis",
|
|
||||||
axisPointer: {
|
|
||||||
type: "shadow",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
left: "3%",
|
|
||||||
right: "12%",
|
|
||||||
bottom: "3%",
|
|
||||||
containLabel: true,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
type: "value",
|
|
||||||
boundaryGap: [0, 0.01],
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
type: "category",
|
|
||||||
data: top10Depts.map((dept) => dept.name),
|
|
||||||
inverse: true,
|
|
||||||
},
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: "人员数量",
|
|
||||||
type: "bar",
|
|
||||||
data: top10Depts.map((dept) => dept.count),
|
|
||||||
itemStyle: {
|
|
||||||
color: function (params) {
|
|
||||||
const colorList = [
|
|
||||||
"#91cc75",
|
|
||||||
"#5470c6",
|
|
||||||
"#ee6666",
|
|
||||||
"#73c0de",
|
|
||||||
"#3ba272",
|
|
||||||
"#fc8452",
|
|
||||||
"#9a60b4",
|
|
||||||
];
|
|
||||||
return colorList[params.dataIndex % colorList.length];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
show: true,
|
|
||||||
position: "right",
|
|
||||||
formatter: "{c}人",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -6,7 +6,7 @@ import StatisticCards from "./StatisticCards";
|
||||||
import ChartControls from "./ChartControls";
|
import ChartControls from "./ChartControls";
|
||||||
import DepartmentCharts from "./DepartmentCharts";
|
import DepartmentCharts from "./DepartmentCharts";
|
||||||
import DepartmentTable from "./DepartmentTable";
|
import DepartmentTable from "./DepartmentTable";
|
||||||
import { getPieChartOptions, getBarChartOptions } from "./char-options";
|
import { getPieChartOptions, getBarChartOptions } from "./DepartmentTable";
|
||||||
interface DeptData {
|
interface DeptData {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -51,6 +51,13 @@ const items = [
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
|
getItem(
|
||||||
|
"test",
|
||||||
|
"/test",
|
||||||
|
<i className="iconfont icon-icon-category" />,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
),
|
||||||
getItem(
|
getItem(
|
||||||
"系统设置",
|
"系统设置",
|
||||||
"/admin",
|
"/admin",
|
||||||
|
|
|
@ -352,8 +352,9 @@ export default function StaffMessage() {
|
||||||
const createMany = api.staff.create.useMutation({
|
const createMany = api.staff.create.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
message.success("员工数据导入成功");
|
message.success("员工数据导入成功");
|
||||||
// 刷新数据
|
// 不要在这里直接调用 useQuery 钩子
|
||||||
api.staff.findMany.useQuery();
|
// 而是使用 trpc 的 invalidate 方法来刷新数据
|
||||||
|
api.useContext().staff.findMany.invalidate();
|
||||||
setImportVisible(false);
|
setImportVisible(false);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
@ -404,25 +405,33 @@ export default function StaffMessage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 我们不在这里处理自定义字段,而是在员工创建后单独处理
|
|
||||||
console.log(`准备创建员工: ${staff.showname}`);
|
console.log(`准备创建员工: ${staff.showname}`);
|
||||||
|
|
||||||
return staff;
|
return staff;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 逐条导入数据
|
// 逐条导入数据
|
||||||
if (staffImportData.length > 0) {
|
if (staffImportData.length > 0) {
|
||||||
|
let importedCount = 0;
|
||||||
|
const totalCount = staffImportData.length;
|
||||||
|
|
||||||
staffImportData.forEach((staffData, index) => {
|
staffImportData.forEach((staffData, index) => {
|
||||||
createMany.mutate(
|
createMany.mutate(
|
||||||
{ data: staffData },
|
{ data: staffData },
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
console.log(`员工创建成功:`, data);
|
console.log(`员工创建成功:`, data);
|
||||||
message.success(`成功导入第${index + 1}条基础数据`);
|
importedCount++;
|
||||||
|
|
||||||
// 员工创建成功后,再单独处理自定义字段值
|
// 显示进度信息
|
||||||
// 由于外键约束问题,我们暂时跳过字段值的创建
|
if (importedCount === totalCount) {
|
||||||
// 后续可以添加专门的字段值导入功能
|
message.success(`所有${totalCount}条数据导入完成`);
|
||||||
|
// 在所有数据导入完成后刷新查询
|
||||||
|
api.useContext().staff.findMany.invalidate();
|
||||||
|
} else {
|
||||||
|
message.info(
|
||||||
|
`已导入 ${importedCount}/${totalCount} 条数据`
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
message.error(
|
message.error(
|
||||||
|
@ -434,7 +443,7 @@ export default function StaffMessage() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
message.info(`正在导入${staffImportData.length}条员工数据...`);
|
message.info(`开始导入${staffImportData.length}条员工数据...`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("处理导入数据失败:", error);
|
console.error("处理导入数据失败:", error);
|
||||||
|
@ -559,12 +568,10 @@ export default function StaffMessage() {
|
||||||
try {
|
try {
|
||||||
const wb = read(e.target?.result);
|
const wb = read(e.target?.result);
|
||||||
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
|
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
message.warning("Excel文件中没有数据");
|
message.warning("Excel文件中没有数据");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
message.info(`读取到${data.length}条数据,正在处理...`);
|
message.info(`读取到${data.length}条数据,正在处理...`);
|
||||||
handleImportData(data);
|
handleImportData(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -7,17 +7,20 @@ import {
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import ErrorPage from "../app/error";
|
import ErrorPage from "../app/error";
|
||||||
import LoginPage from "../app/login";
|
import LoginPage from "../app/login";
|
||||||
import HomePage from "../app/main/home/page";
|
// import HomePage from "../app/main/home/page";
|
||||||
import StaffMessage from "../app/main/staffinfo_show/staffmessage_page";
|
import StaffMessage from "../app/main/staffinfo_show/staffmessage_page";
|
||||||
import MainLayout from "../app/main/layout/MainLayout";
|
import MainLayout from "../app/main/layout/MainLayout";
|
||||||
import DailyPage from "../app/main/daily/page";
|
// import DailyPage from "../app/main/daily/page";
|
||||||
import Dashboard from "../app/main/home/page";
|
import Dashboard from "../app/main/home/page";
|
||||||
import WeekPlanPage from "../app/main/plan/weekplan/page";
|
// import WeekPlanPage from "../app/main/plan/weekplan/page";
|
||||||
import StaffInformation from "../app/main/staffinfo_write/staffinfo_write.page";
|
import StaffInformation from "../app/main/staffinfo_write/staffinfo_write.page";
|
||||||
import DeptSettingPage from "../app/main/admin/deptsettingpage/page";
|
import DeptSettingPage from "../app/main/admin/deptsettingpage/page";
|
||||||
import { adminRoute } from "./admin-route";
|
import { adminRoute } from "./admin-route";
|
||||||
import AdminLayout from "../components/layout/admin/AdminLayout";
|
import AdminLayout from "../components/layout/admin/AdminLayout";
|
||||||
import SystemLogPage from "../app/main/systemlog/SystemLogPage";
|
import SystemLogPage from "../app/main/systemlog/SystemLogPage";
|
||||||
|
import TestPage from "../app/main/Test/Page";
|
||||||
|
|
||||||
|
|
||||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||||
name?: string;
|
name?: string;
|
||||||
breadcrumb?: string;
|
breadcrumb?: string;
|
||||||
|
@ -71,6 +74,10 @@ export const routes: CustomRouteObject[] = [
|
||||||
element: <AdminLayout></AdminLayout>,
|
element: <AdminLayout></AdminLayout>,
|
||||||
children: adminRoute.children,
|
children: adminRoute.children,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/test",
|
||||||
|
element: <TestPage></TestPage>,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -2,7 +2,7 @@ server {
|
||||||
# 监听80端口
|
# 监听80端口
|
||||||
listen 80;
|
listen 80;
|
||||||
# 服务器域名/IP地址,使用环境变量
|
# 服务器域名/IP地址,使用环境变量
|
||||||
server_name 192.168.252.77;
|
server_name 192.168.217.194;
|
||||||
|
|
||||||
# 基础性能优化配置
|
# 基础性能优化配置
|
||||||
# 启用tcp_nopush以优化数据发送
|
# 启用tcp_nopush以优化数据发送
|
||||||
|
@ -100,7 +100,7 @@ server {
|
||||||
# 仅供内部使用
|
# 仅供内部使用
|
||||||
internal;
|
internal;
|
||||||
# 代理到认证服务
|
# 代理到认证服务
|
||||||
proxy_pass http://192.168.252.77:3001/auth/file;
|
proxy_pass http://192.168.217.194:3001/auth/file;
|
||||||
|
|
||||||
# 请求优化:不传递请求体
|
# 请求优化:不传递请求体
|
||||||
proxy_pass_request_body off;
|
proxy_pass_request_body off;
|
||||||
|
|
Loading…
Reference in New Issue