diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index 3bc892f..94013f0 100755 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -23,12 +23,12 @@ import { UploadModule } from './upload/upload.module'; imports: [ ConfigModule.forRoot({ isGlobal: true, // 全局可用 - envFilePath: '.env' + envFilePath: '.env', }), ScheduleModule.forRoot(), JwtModule.register({ global: true, - secret: env.JWT_SECRET + secret: env.JWT_SECRET, }), WebSocketModule, TrpcModule, @@ -42,11 +42,13 @@ import { UploadModule } from './upload/upload.module'; MinioModule, CollaborationModule, RealTimeModule, - UploadModule + UploadModule, + ], + providers: [ + { + provide: APP_FILTER, + useClass: ExceptionsFilter, + }, ], - providers: [{ - provide: APP_FILTER, - useClass: ExceptionsFilter, - }], }) -export class AppModule { } +export class AppModule {} diff --git a/apps/server/src/auth/auth.guard.ts b/apps/server/src/auth/auth.guard.ts index e9fda52..5c8a455 100755 --- a/apps/server/src/auth/auth.guard.ts +++ b/apps/server/src/auth/auth.guard.ts @@ -1,8 +1,8 @@ import { - CanActivate, - ExecutionContext, - Injectable, - UnauthorizedException, + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { env } from '@server/env'; @@ -12,26 +12,21 @@ import { extractTokenFromHeader } from './utils'; @Injectable() export class AuthGuard implements CanActivate { - constructor(private jwtService: JwtService) { } - async canActivate(context: ExecutionContext): Promise { - const request = context.switchToHttp().getRequest(); - const token = extractTokenFromHeader(request); - if (!token) { - throw new UnauthorizedException(); - } - try { - const payload: JwtPayload = await this.jwtService.verifyAsync( - token, - { - secret: env.JWT_SECRET - } - ); - request['user'] = payload; - } catch { - throw new UnauthorizedException(); - } - return true; + constructor(private jwtService: JwtService) {} + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException(); } - - -} \ No newline at end of file + try { + const payload: JwtPayload = await this.jwtService.verifyAsync(token, { + secret: env.JWT_SECRET, + }); + request['user'] = payload; + } catch { + throw new UnauthorizedException(); + } + return true; + } +} diff --git a/apps/server/src/auth/auth.module.ts b/apps/server/src/auth/auth.module.ts index ade8298..98fa861 100755 --- a/apps/server/src/auth/auth.module.ts +++ b/apps/server/src/auth/auth.module.ts @@ -8,12 +8,8 @@ import { SessionService } from './session.service'; import { RoleMapModule } from '@server/models/rbac/rbac.module'; @Module({ imports: [StaffModule, RoleMapModule], - providers: [ - AuthService, - TrpcService, - DepartmentService, - SessionService], + providers: [AuthService, TrpcService, DepartmentService, SessionService], exports: [AuthService], controllers: [AuthController], }) -export class AuthModule { } +export class AuthModule {} diff --git a/apps/server/src/auth/config.ts b/apps/server/src/auth/config.ts index 0edaf5d..64d6776 100755 --- a/apps/server/src/auth/config.ts +++ b/apps/server/src/auth/config.ts @@ -1,9 +1,9 @@ export const tokenConfig = { - accessToken: { - expirationMs: 7 * 24 * 3600000, // 7 days - expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds - }, - refreshToken: { - expirationMs: 30 * 24 * 3600000, // 30 days - }, -}; \ No newline at end of file + accessToken: { + expirationMs: 7 * 24 * 3600000, // 7 days + expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds + }, + refreshToken: { + expirationMs: 30 * 24 * 3600000, // 30 days + }, +}; diff --git a/apps/server/src/auth/session.service.ts b/apps/server/src/auth/session.service.ts index 9ee9022..92c629d 100755 --- a/apps/server/src/auth/session.service.ts +++ b/apps/server/src/auth/session.service.ts @@ -4,58 +4,63 @@ import { redis } from '@server/utils/redis/redis.service'; import { v4 as uuidv4 } from 'uuid'; export interface SessionInfo { - session_id: string; - access_token: string; - access_token_expires_at: number; - refresh_token: string; - refresh_token_expires_at: number; + session_id: string; + access_token: string; + access_token_expires_at: number; + refresh_token: string; + refresh_token_expires_at: number; } @Injectable() export class SessionService { - private getSessionKey(userId: string, sessionId: string): string { - return `session-${userId}-${sessionId}`; - } - async createSession( - userId: string, - accessToken: string, - refreshToken: string, - expirationConfig: { - accessTokenExpirationMs: number; - refreshTokenExpirationMs: number; - sessionTTL: number; - }, - ): Promise { - const sessionInfo: SessionInfo = { - session_id: uuidv4(), - access_token: accessToken, - access_token_expires_at: Date.now() + expirationConfig.accessTokenExpirationMs, - refresh_token: refreshToken, - refresh_token_expires_at: Date.now() + expirationConfig.refreshTokenExpirationMs, - }; + private getSessionKey(userId: string, sessionId: string): string { + return `session-${userId}-${sessionId}`; + } + async createSession( + userId: string, + accessToken: string, + refreshToken: string, + expirationConfig: { + accessTokenExpirationMs: number; + refreshTokenExpirationMs: number; + sessionTTL: number; + }, + ): Promise { + const sessionInfo: SessionInfo = { + session_id: uuidv4(), + access_token: accessToken, + access_token_expires_at: + Date.now() + expirationConfig.accessTokenExpirationMs, + refresh_token: refreshToken, + refresh_token_expires_at: + Date.now() + expirationConfig.refreshTokenExpirationMs, + }; - await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL); - return sessionInfo; - } + await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL); + return sessionInfo; + } - async getSession(userId: string, sessionId: string): Promise { - const sessionData = await redis.get(this.getSessionKey(userId, sessionId)); - return sessionData ? JSON.parse(sessionData) : null; - } + async getSession( + userId: string, + sessionId: string, + ): Promise { + const sessionData = await redis.get(this.getSessionKey(userId, sessionId)); + return sessionData ? JSON.parse(sessionData) : null; + } - async saveSession( - userId: string, - sessionInfo: SessionInfo, - ttl: number, - ): Promise { - await redis.setex( - this.getSessionKey(userId, sessionInfo.session_id), - ttl, - JSON.stringify(sessionInfo), - ); - } + async saveSession( + userId: string, + sessionInfo: SessionInfo, + ttl: number, + ): Promise { + await redis.setex( + this.getSessionKey(userId, sessionInfo.session_id), + ttl, + JSON.stringify(sessionInfo), + ); + } - async deleteSession(userId: string, sessionId: string): Promise { - await redis.del(this.getSessionKey(userId, sessionId)); - } -} \ No newline at end of file + async deleteSession(userId: string, sessionId: string): Promise { + await redis.del(this.getSessionKey(userId, sessionId)); + } +} diff --git a/apps/server/src/auth/types.ts b/apps/server/src/auth/types.ts index 4cb6c47..d539049 100755 --- a/apps/server/src/auth/types.ts +++ b/apps/server/src/auth/types.ts @@ -1,31 +1,31 @@ export interface TokenConfig { - accessToken: { - expirationMs: number; - expirationTTL: number; - }; - refreshToken: { - expirationMs: number; - }; + accessToken: { + expirationMs: number; + expirationTTL: number; + }; + refreshToken: { + expirationMs: number; + }; } export interface FileAuthResult { - isValid: boolean - userId?: string - resourceType?: string - error?: string + isValid: boolean; + userId?: string; + resourceType?: string; + error?: string; } export interface FileRequest { - originalUri: string; - realIp: string; - method: string; - queryParams: string; - host: string; - authorization: string + originalUri: string; + realIp: string; + method: string; + queryParams: string; + host: string; + authorization: string; } export enum FileValidationErrorType { - INVALID_URI = 'INVALID_URI', - RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', - AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED', - INVALID_TOKEN = 'INVALID_TOKEN', - UNKNOWN_ERROR = 'UNKNOWN_ERROR' -} \ No newline at end of file + INVALID_URI = 'INVALID_URI', + RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', + AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED', + INVALID_TOKEN = 'INVALID_TOKEN', + UNKNOWN_ERROR = 'UNKNOWN_ERROR', +} diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts index ef8968d..1c443d2 100755 --- a/apps/server/src/auth/utils.ts +++ b/apps/server/src/auth/utils.ts @@ -11,7 +11,7 @@ import { env } from '@server/env'; import { redis } from '@server/utils/redis/redis.service'; import EventBus from '@server/utils/event-bus'; import { RoleMapService } from '@server/models/rbac/rolemap.service'; -import { Request } from "express" +import { Request } from 'express'; interface ProfileResult { staff: UserProfile | undefined; error?: string; @@ -22,9 +22,11 @@ interface TokenVerifyResult { error?: string; } 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(' ') ?? []; return type === 'Bearer' ? token : undefined; } @@ -40,7 +42,7 @@ export class UserProfileService { this.jwtService = new JwtService(); this.departmentService = new DepartmentService(); this.roleMapService = new RoleMapService(this.departmentService); - EventBus.on("dataChanged", ({ type, data }) => { + EventBus.on('dataChanged', ({ type, data }) => { if (type === ObjectType.STAFF) { // 确保 data 是数组,如果不是则转换为数组 const dataArray = Array.isArray(data) ? data : [data]; @@ -51,7 +53,6 @@ export class UserProfileService { } } }); - } public getProfileCacheKey(id: string) { return `user-profile-${id}`; @@ -175,9 +176,7 @@ export class UserProfileService { staff.deptId ? this.departmentService.getDescendantIdsInDomain(staff.deptId) : [], - staff.deptId - ? this.departmentService.getAncestorIds([staff.deptId]) - : [], + staff.deptId ? this.departmentService.getAncestorIds([staff.deptId]) : [], this.roleMapService.getPermsForObject({ domainId: staff.domainId, staffId: staff.id, diff --git a/apps/server/src/models/app-config/app-config.module.ts b/apps/server/src/models/app-config/app-config.module.ts index 732313c..33e97b1 100755 --- a/apps/server/src/models/app-config/app-config.module.ts +++ b/apps/server/src/models/app-config/app-config.module.ts @@ -7,6 +7,6 @@ import { RealTimeModule } from '@server/socket/realtime/realtime.module'; @Module({ imports: [RealTimeModule], providers: [AppConfigService, AppConfigRouter, TrpcService], - exports: [AppConfigService, AppConfigRouter] + exports: [AppConfigService, AppConfigRouter], }) -export class AppConfigModule { } +export class AppConfigModule {} diff --git a/apps/server/src/models/base/errorMap.prisma.ts b/apps/server/src/models/base/errorMap.prisma.ts index ab234ce..9c43efd 100755 --- a/apps/server/src/models/base/errorMap.prisma.ts +++ b/apps/server/src/models/base/errorMap.prisma.ts @@ -1,198 +1,222 @@ import { - BadRequestException, - NotFoundException, - ConflictException, - InternalServerErrorException, - } from '@nestjs/common'; - - export const PrismaErrorCode = Object.freeze({ - P2000: 'P2000', - P2001: 'P2001', - P2002: 'P2002', - P2003: 'P2003', - P2006: 'P2006', - P2007: 'P2007', - P2008: 'P2008', - P2009: 'P2009', - P2010: 'P2010', - P2011: 'P2011', - P2012: 'P2012', - P2014: 'P2014', - P2015: 'P2015', - P2016: 'P2016', - P2017: 'P2017', - P2018: 'P2018', - P2019: 'P2019', - P2021: 'P2021', - P2023: 'P2023', - P2025: 'P2025', - P2031: 'P2031', - P2033: 'P2033', - P2034: 'P2034', - P2037: 'P2037', - P1000: 'P1000', - P1001: 'P1001', - P1002: 'P1002', - P1015: 'P1015', - P1017: 'P1017', - }); - - export type PrismaErrorCode = keyof typeof PrismaErrorCode; - - - interface PrismaErrorMeta { - target?: string; - model?: string; - relationName?: string; - details?: string; - } - - export type operationT = 'create' | 'read' | 'update' | 'delete'; - - export type PrismaErrorHandler = ( - operation: operationT, - meta?: PrismaErrorMeta, - ) => Error; - - export const ERROR_MAP: Record = { - P2000: (_operation, meta) => new BadRequestException( - `The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.` - ), + BadRequestException, + NotFoundException, + ConflictException, + InternalServerErrorException, +} from '@nestjs/common'; - P2001: (operation, meta) => new NotFoundException( - `The ${meta?.model || 'record'} you are trying to ${operation} could not be found.` - ), +export const PrismaErrorCode = Object.freeze({ + P2000: 'P2000', + P2001: 'P2001', + P2002: 'P2002', + P2003: 'P2003', + P2006: 'P2006', + P2007: 'P2007', + P2008: 'P2008', + P2009: 'P2009', + P2010: 'P2010', + P2011: 'P2011', + P2012: 'P2012', + P2014: 'P2014', + P2015: 'P2015', + P2016: 'P2016', + P2017: 'P2017', + P2018: 'P2018', + P2019: 'P2019', + P2021: 'P2021', + P2023: 'P2023', + P2025: 'P2025', + P2031: 'P2031', + P2033: 'P2033', + P2034: 'P2034', + P2037: 'P2037', + P1000: 'P1000', + P1001: 'P1001', + P1002: 'P1002', + P1015: 'P1015', + P1017: 'P1017', +}); - P2002: (operation, meta) => { - const field = meta?.target || 'unique field'; - switch (operation) { - case 'create': - return new ConflictException( - `A record with the same ${field} already exists. Please use a different value.` - ); - case 'update': - return new ConflictException( - `The new value for ${field} conflicts with an existing record.` - ); - default: - return new ConflictException( - `Unique constraint violation on ${field}.` - ); - } - }, +export type PrismaErrorCode = keyof typeof PrismaErrorCode; - P2003: (operation) => new BadRequestException( - `Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.` - ), +interface PrismaErrorMeta { + target?: string; + model?: string; + relationName?: string; + details?: string; +} - P2006: (_operation, meta) => new BadRequestException( - `The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.` - ), +export type operationT = 'create' | 'read' | 'update' | 'delete'; - P2007: (operation) => new InternalServerErrorException( - `Data validation error during ${operation}. Please ensure all inputs are valid and try again.` - ), +export type PrismaErrorHandler = ( + operation: operationT, + meta?: PrismaErrorMeta, +) => Error; - P2008: (operation) => new InternalServerErrorException( - `Failed to query the database during ${operation}. Please try again later.` - ), +export const ERROR_MAP: Record = { + P2000: (_operation, meta) => + new BadRequestException( + `The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.`, + ), - P2009: (operation) => new InternalServerErrorException( - `Invalid data fetched during ${operation}. Check query structure.` - ), + P2001: (operation, meta) => + new NotFoundException( + `The ${meta?.model || 'record'} you are trying to ${operation} could not be found.`, + ), - P2010: () => new InternalServerErrorException( - `Invalid raw query. Ensure your query is correct and try again.` - ), + P2002: (operation, meta) => { + const field = meta?.target || 'unique field'; + switch (operation) { + case 'create': + return new ConflictException( + `A record with the same ${field} already exists. Please use a different value.`, + ); + case 'update': + return new ConflictException( + `The new value for ${field} conflicts with an existing record.`, + ); + default: + return new ConflictException( + `Unique constraint violation on ${field}.`, + ); + } + }, - P2011: (_operation, meta) => new BadRequestException( - `The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.` - ), + P2003: (operation) => + new BadRequestException( + `Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`, + ), - P2012: (operation, meta) => new BadRequestException( - `Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${operation}.` - ), + P2006: (_operation, meta) => + new BadRequestException( + `The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.`, + ), - P2014: (operation) => { - switch (operation) { - case 'create': - return new BadRequestException( - `Cannot create record because the referenced data does not exist. Ensure related data exists.` - ); - case 'delete': - return new BadRequestException( - `Unable to delete record because it is linked to other data. Update or delete dependent records first.` - ); - default: - return new BadRequestException(`Foreign key constraint error.`); - } - }, + P2007: (operation) => + new InternalServerErrorException( + `Data validation error during ${operation}. Please ensure all inputs are valid and try again.`, + ), - P2015: () => new InternalServerErrorException( - `A record with the required ID was expected but not found. Please retry.` - ), + P2008: (operation) => + new InternalServerErrorException( + `Failed to query the database during ${operation}. Please try again later.`, + ), - P2016: (operation) => new InternalServerErrorException( - `Query ${operation} failed because the record could not be fetched. Ensure the query is correct.` - ), + P2009: (operation) => + new InternalServerErrorException( + `Invalid data fetched during ${operation}. Check query structure.`, + ), - P2017: (operation) => new InternalServerErrorException( - `Connected records were not found for ${operation}. Check related data.` - ), + P2010: () => + new InternalServerErrorException( + `Invalid raw query. Ensure your query is correct and try again.`, + ), - P2018: () => new InternalServerErrorException( - `The required connection could not be established. Please check relationships.` - ), + P2011: (_operation, meta) => + new BadRequestException( + `The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.`, + ), - P2019: (_operation, meta) => new InternalServerErrorException( - `Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.` - ), + P2012: (operation, meta) => + new BadRequestException( + `Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${operation}.`, + ), - P2021: (_operation, meta) => new InternalServerErrorException( - `The ${meta?.model || 'model'} was not found in the database.` - ), + P2014: (operation) => { + switch (operation) { + case 'create': + return new BadRequestException( + `Cannot create record because the referenced data does not exist. Ensure related data exists.`, + ); + case 'delete': + return new BadRequestException( + `Unable to delete record because it is linked to other data. Update or delete dependent records first.`, + ); + default: + return new BadRequestException(`Foreign key constraint error.`); + } + }, - P2025: (operation, meta) => new NotFoundException( - `The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.` - ), + P2015: () => + new InternalServerErrorException( + `A record with the required ID was expected but not found. Please retry.`, + ), - P2031: () => new InternalServerErrorException( - `Invalid Prisma Client initialization error. Please check configuration.` - ), + P2016: (operation) => + new InternalServerErrorException( + `Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`, + ), - P2033: (operation) => new InternalServerErrorException( - `Insufficient database write permissions for ${operation}.` - ), + P2017: (operation) => + new InternalServerErrorException( + `Connected records were not found for ${operation}. Check related data.`, + ), - P2034: (operation) => new InternalServerErrorException( - `Database read-only transaction failed during ${operation}.` - ), + P2018: () => + new InternalServerErrorException( + `The required connection could not be established. Please check relationships.`, + ), - P2037: (operation) => new InternalServerErrorException( - `Unsupported combinations of input types for ${operation}. Please correct the query or input.` - ), + P2019: (_operation, meta) => + new InternalServerErrorException( + `Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.`, + ), - P1000: () => new InternalServerErrorException( - `Database authentication failed. Verify your credentials and try again.` - ), + P2021: (_operation, meta) => + new InternalServerErrorException( + `The ${meta?.model || 'model'} was not found in the database.`, + ), - P1001: () => new InternalServerErrorException( - `The database server could not be reached. Please check its availability.` - ), + P2025: (operation, meta) => + new NotFoundException( + `The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.`, + ), - P1002: () => new InternalServerErrorException( - `Connection to the database timed out. Verify network connectivity and server availability.` - ), + P2031: () => + new InternalServerErrorException( + `Invalid Prisma Client initialization error. Please check configuration.`, + ), - P1015: (operation) => new InternalServerErrorException( - `Migration failed. Unable to complete ${operation}. Check migration history or database state.` - ), + P2033: (operation) => + new InternalServerErrorException( + `Insufficient database write permissions for ${operation}.`, + ), - P1017: () => new InternalServerErrorException( - `Database connection failed. Ensure the database is online and credentials are correct.` - ), - P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error { - throw new Error('Function not implemented.'); - } - }; - \ No newline at end of file + P2034: (operation) => + new InternalServerErrorException( + `Database read-only transaction failed during ${operation}.`, + ), + + P2037: (operation) => + new InternalServerErrorException( + `Unsupported combinations of input types for ${operation}. Please correct the query or input.`, + ), + + P1000: () => + new InternalServerErrorException( + `Database authentication failed. Verify your credentials and try again.`, + ), + + P1001: () => + new InternalServerErrorException( + `The database server could not be reached. Please check its availability.`, + ), + + P1002: () => + new InternalServerErrorException( + `Connection to the database timed out. Verify network connectivity and server availability.`, + ), + + P1015: (operation) => + new InternalServerErrorException( + `Migration failed. Unable to complete ${operation}. Check migration history or database state.`, + ), + + P1017: () => + new InternalServerErrorException( + `Database connection failed. Ensure the database is online and credentials are correct.`, + ), + P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error { + throw new Error('Function not implemented.'); + }, +}; diff --git a/apps/server/src/models/base/row-cache.service.ts b/apps/server/src/models/base/row-cache.service.ts index 6150407..f843d41 100755 --- a/apps/server/src/models/base/row-cache.service.ts +++ b/apps/server/src/models/base/row-cache.service.ts @@ -1,183 +1,176 @@ -import { UserProfile, RowModelRequest, RowRequestSchema } from "@nice/common"; -import { RowModelService } from "./row-model.service"; -import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder"; -import EventBus from "@server/utils/event-bus"; -import supejson from "superjson-cjs" -import { deleteByPattern } from "@server/utils/redis/utils"; -import { redis } from "@server/utils/redis/redis.service"; -import { z } from "zod"; +import { UserProfile, RowModelRequest, RowRequestSchema } from '@nice/common'; +import { RowModelService } from './row-model.service'; +import { isFieldCondition, LogicalCondition, SQLBuilder } from './sql-builder'; +import EventBus from '@server/utils/event-bus'; +import supejson from 'superjson-cjs'; +import { deleteByPattern } from '@server/utils/redis/utils'; +import { redis } from '@server/utils/redis/redis.service'; +import { z } from 'zod'; export class RowCacheService extends RowModelService { - constructor(tableName: string, private enableCache: boolean = true) { - super(tableName) - if (this.enableCache) { - EventBus.on("dataChanged", async ({ type, data }) => { - if (type === tableName) { - const dataArray = Array.isArray(data) ? data : [data]; - for (const item of dataArray) { - try { - if (item.id) { - this.invalidateRowCacheById(item.id) - } - if (item.parentId) { - this.invalidateRowCacheById(item.parentId) - } - } catch (err) { - console.error(`Error deleting cache for type ${tableName}:`, err); - } - } - } - }); + constructor( + tableName: string, + private enableCache: boolean = true, + ) { + super(tableName); + if (this.enableCache) { + EventBus.on('dataChanged', async ({ type, data }) => { + if (type === tableName) { + const dataArray = Array.isArray(data) ? data : [data]; + for (const item of dataArray) { + try { + if (item.id) { + this.invalidateRowCacheById(item.id); + } + if (item.parentId) { + this.invalidateRowCacheById(item.parentId); + } + } catch (err) { + console.error(`Error deleting cache for type ${tableName}:`, err); + } + } } + }); } - protected getRowCacheKey(id: string) { - return `row-data-${id}`; + } + protected getRowCacheKey(id: string) { + return `row-data-${id}`; + } + private async invalidateRowCacheById(id: string) { + if (!this.enableCache) return; + const pattern = this.getRowCacheKey(id); + await deleteByPattern(pattern); + } + createJoinSql(request?: RowModelRequest): string[] { + return []; + } + protected async getRowRelation(args: { data: any; staff?: UserProfile }) { + return args.data; + } + protected async setResPermissions(data: any, staff?: UserProfile) { + return data; + } + protected async getRowDto(data: any, staff?: UserProfile): Promise { + // 如果没有id,直接返回原数据 + if (!data?.id) return data; + // 如果未启用缓存,直接处理并返回数据 + if (!this.enableCache) { + return this.processDataWithPermissions(data, staff); } - private async invalidateRowCacheById(id: string) { - if (!this.enableCache) return; - const pattern = this.getRowCacheKey(id); - await deleteByPattern(pattern); - } - createJoinSql(request?: RowModelRequest): string[] { - return [] - } - protected async getRowRelation(args: { data: any, staff?: UserProfile }) { - return args.data; - } - protected async setResPermissions( - data: any, - staff?: UserProfile, - ) { - return data - } - protected async getRowDto( - data: any, - staff?: UserProfile, - ): Promise { - // 如果没有id,直接返回原数据 - if (!data?.id) return data; - // 如果未启用缓存,直接处理并返回数据 - if (!this.enableCache) { - return this.processDataWithPermissions(data, staff); - } - const key = this.getRowCacheKey(data.id); - try { - // 尝试从缓存获取数据 - const cachedData = await this.getCachedData(key, staff); - // 如果缓存命中,直接返回 - if (cachedData) return cachedData; - // 处理数据并缓存 - const processedData = await this.processDataWithPermissions(data, staff); - await redis.set(key, supejson.stringify(processedData)); - return processedData; - } catch (err) { - this.logger.error('Error in getRowDto:', err); - throw err; - } + const key = this.getRowCacheKey(data.id); + try { + // 尝试从缓存获取数据 + const cachedData = await this.getCachedData(key, staff); + // 如果缓存命中,直接返回 + if (cachedData) return cachedData; + // 处理数据并缓存 + const processedData = await this.processDataWithPermissions(data, staff); + await redis.set(key, supejson.stringify(processedData)); + return processedData; + } catch (err) { + this.logger.error('Error in getRowDto:', err); + throw err; } + } - private async getCachedData( - key: string, - staff?: UserProfile - ): Promise { - const cachedDataStr = await redis.get(key); - if (!cachedDataStr) return null; - const cachedData = supejson.parse(cachedDataStr) as any; - if (!cachedData?.id) return null; - return staff - ? this.setResPermissions(cachedData, staff) - : cachedData; - } + private async getCachedData( + key: string, + staff?: UserProfile, + ): Promise { + const cachedDataStr = await redis.get(key); + if (!cachedDataStr) return null; + const cachedData = supejson.parse(cachedDataStr) as any; + if (!cachedData?.id) return null; + return staff ? this.setResPermissions(cachedData, staff) : cachedData; + } - private async processDataWithPermissions( - data: any, - staff?: UserProfile - ): Promise { - // 处理权限 - const permData = staff - ? await this.setResPermissions(data, staff) - : data; - // 获取关联数据 - return this.getRowRelation({ data: permData, staff }); - } + private async processDataWithPermissions( + data: any, + staff?: UserProfile, + ): Promise { + // 处理权限 + const permData = staff ? await this.setResPermissions(data, staff) : data; + // 获取关联数据 + return this.getRowRelation({ data: permData, staff }); + } - protected createGetRowsFilters( - request: z.infer, - staff?: UserProfile, - ) { - const condition = super.createGetRowsFilters(request); - if (isFieldCondition(condition)) return {}; - const baseCondition: LogicalCondition[] = [ - { - field: `${this.tableName}.deleted_at`, - op: 'blank', - type: 'date', - }, - ]; - condition.AND = [...baseCondition, ...condition.AND]; - return condition; - } - createUnGroupingRowSelect(request?: RowModelRequest): string[] { - return [ - `${this.tableName}.id AS id`, - SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`), - ]; - } - protected createGroupingRowSelect( - request: RowModelRequest, - wrapperSql: boolean, - ): string[] { - const colsToSelect = super.createGroupingRowSelect(request, wrapperSql); - return colsToSelect.concat([ - SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`), - ]); - } - protected async getRowsSqlWrapper( - sql: string, - request?: RowModelRequest, - staff?: UserProfile, - ): Promise { - const groupingSql = SQLBuilder.join([ - SQLBuilder.select([ - ...this.createGroupingRowSelect(request, true), - `${this.tableName}.id AS id`, - ]), - SQLBuilder.from(this.tableName), - SQLBuilder.join(this.createJoinSql(request)), - SQLBuilder.where(this.createGetRowsFilters(request, staff)), - ]); - const { rowGroupCols, valueCols, groupKeys } = request; - if (this.isDoingGroup(request)) { - const rowGroupCol = rowGroupCols[groupKeys.length]; - const groupByField = rowGroupCol?.field?.replace('.', '_'); - return SQLBuilder.join([ - SQLBuilder.select([ - groupByField, - ...super.createAggSqlForWrapper(request), - 'COUNT(id) AS child_count', - ]), - SQLBuilder.from(`(${groupingSql})`), - SQLBuilder.where({ - field: 'row_num', - value: '1', - op: 'equals', - }), - SQLBuilder.groupBy([groupByField]), - SQLBuilder.orderBy( - this.getOrderByColumns(request).map((item) => item.replace('.', '_')), - ), - this.getLimitSql(request), - ]); - } else - return SQLBuilder.join([ - SQLBuilder.select(['*']), - SQLBuilder.from(`(${sql})`), - SQLBuilder.where({ - field: 'row_num', - value: '1', - op: 'equals', - }), - this.getLimitSql(request), - ]); - // return super.getRowsSqlWrapper(sql, request) - } -} \ No newline at end of file + protected createGetRowsFilters( + request: z.infer, + staff?: UserProfile, + ) { + const condition = super.createGetRowsFilters(request); + if (isFieldCondition(condition)) return {}; + const baseCondition: LogicalCondition[] = [ + { + field: `${this.tableName}.deleted_at`, + op: 'blank', + type: 'date', + }, + ]; + condition.AND = [...baseCondition, ...condition.AND]; + return condition; + } + createUnGroupingRowSelect(request?: RowModelRequest): string[] { + return [ + `${this.tableName}.id AS id`, + SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`), + ]; + } + protected createGroupingRowSelect( + request: RowModelRequest, + wrapperSql: boolean, + ): string[] { + const colsToSelect = super.createGroupingRowSelect(request, wrapperSql); + return colsToSelect.concat([ + SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`), + ]); + } + protected async getRowsSqlWrapper( + sql: string, + request?: RowModelRequest, + staff?: UserProfile, + ): Promise { + const groupingSql = SQLBuilder.join([ + SQLBuilder.select([ + ...this.createGroupingRowSelect(request, true), + `${this.tableName}.id AS id`, + ]), + SQLBuilder.from(this.tableName), + SQLBuilder.join(this.createJoinSql(request)), + SQLBuilder.where(this.createGetRowsFilters(request, staff)), + ]); + const { rowGroupCols, valueCols, groupKeys } = request; + if (this.isDoingGroup(request)) { + const rowGroupCol = rowGroupCols[groupKeys.length]; + const groupByField = rowGroupCol?.field?.replace('.', '_'); + return SQLBuilder.join([ + SQLBuilder.select([ + groupByField, + ...super.createAggSqlForWrapper(request), + 'COUNT(id) AS child_count', + ]), + SQLBuilder.from(`(${groupingSql})`), + SQLBuilder.where({ + field: 'row_num', + value: '1', + op: 'equals', + }), + SQLBuilder.groupBy([groupByField]), + SQLBuilder.orderBy( + this.getOrderByColumns(request).map((item) => item.replace('.', '_')), + ), + this.getLimitSql(request), + ]); + } else + return SQLBuilder.join([ + SQLBuilder.select(['*']), + SQLBuilder.from(`(${sql})`), + SQLBuilder.where({ + field: 'row_num', + value: '1', + op: 'equals', + }), + this.getLimitSql(request), + ]); + // return super.getRowsSqlWrapper(sql, request) + } +} diff --git a/apps/server/src/models/base/row-model.service.ts b/apps/server/src/models/base/row-model.service.ts index d43769f..8b846e7 100755 --- a/apps/server/src/models/base/row-model.service.ts +++ b/apps/server/src/models/base/row-model.service.ts @@ -21,7 +21,7 @@ export abstract class RowModelService { // 添加更多需要引号的关键词 ]); protected logger = new Logger(this.tableName); - protected constructor(protected tableName: string) { } + protected constructor(protected tableName: string) {} protected async getRowDto(row: any, staff?: UserProfile): Promise { return row; } @@ -140,11 +140,11 @@ export abstract class RowModelService { private buildFilterConditions(filterModel: any): LogicalCondition[] { return filterModel ? Object.entries(filterModel)?.map(([key, item]) => - SQLBuilder.createFilterSql( - key === 'ag-Grid-AutoColumn' ? 'name' : key, - item, - ), - ) + SQLBuilder.createFilterSql( + key === 'ag-Grid-AutoColumn' ? 'name' : key, + item, + ), + ) : []; } diff --git a/apps/server/src/models/base/sql-builder.ts b/apps/server/src/models/base/sql-builder.ts index 67cd331..d081873 100755 --- a/apps/server/src/models/base/sql-builder.ts +++ b/apps/server/src/models/base/sql-builder.ts @@ -1,138 +1,170 @@ export interface FieldCondition { - field: string; - op: OperatorType - type?: "text" | "number" | "date"; - value?: any; - valueTo?: any; -}; -export type OperatorType = 'equals' | 'notEqual' | 'contains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank' | 'greaterThan' | 'lessThanOrEqual' | 'inRange' | 'lessThan' | 'greaterThan' | 'in'; -export type LogicalCondition = FieldCondition | { - AND?: LogicalCondition[]; - OR?: LogicalCondition[]; -}; + field: string; + op: OperatorType; + type?: 'text' | 'number' | 'date'; + value?: any; + valueTo?: any; +} +export type OperatorType = + | 'equals' + | 'notEqual' + | 'contains' + | 'startsWith' + | 'endsWith' + | 'blank' + | 'notBlank' + | 'greaterThan' + | 'lessThanOrEqual' + | 'inRange' + | 'lessThan' + | 'greaterThan' + | 'in'; +export type LogicalCondition = + | FieldCondition + | { + AND?: LogicalCondition[]; + OR?: LogicalCondition[]; + }; -export function isFieldCondition(condition: LogicalCondition): condition is FieldCondition { - return (condition as FieldCondition).field !== undefined; +export function isFieldCondition( + condition: LogicalCondition, +): condition is FieldCondition { + return (condition as FieldCondition).field !== undefined; } function buildCondition(condition: FieldCondition): string { - const { field, op, value, type = "text", valueTo } = condition; - switch (op) { - case 'equals': - return `${field} = '${value}'`; - case 'notEqual': - return `${field} != '${value}'`; - case 'contains': - return `${field} LIKE '%${value}%'`; - case 'startsWith': - return `${field} LIKE '${value}%'`; - case 'endsWith': - return `${field} LIKE '%${value}'`; - case 'blank': - if (type !== "date") - return `(${field} IS NULL OR ${field} = '')`; - else - return `${field} IS NULL`; - case 'notBlank': - if (type !== 'date') - return `${field} IS NOT NULL AND ${field} != ''`; - else - return `${field} IS NOT NULL`; - case 'greaterThan': - return `${field} > '${value}'`; - case 'lessThanOrEqual': - return `${field} <= '${value}'`; - case 'lessThan': - return `${field} < '${value}'`; - case 'greaterThan': - return `${field} > '${value}'`; - case 'inRange': - return `${field} >= '${value}' AND ${field} <= '${valueTo}'`; - case 'in': - if (!value || (Array.isArray(value) && value.length === 0)) { - // Return a condition that is always false if value is empty or an empty array - return '1 = 0'; - } - return `${field} IN (${(value as any[]).map(val => `'${val}'`).join(', ')})`; - default: - return 'true'; // Default return for unmatched conditions - } + const { field, op, value, type = 'text', valueTo } = condition; + switch (op) { + case 'equals': + return `${field} = '${value}'`; + case 'notEqual': + return `${field} != '${value}'`; + case 'contains': + return `${field} LIKE '%${value}%'`; + case 'startsWith': + return `${field} LIKE '${value}%'`; + case 'endsWith': + return `${field} LIKE '%${value}'`; + case 'blank': + if (type !== 'date') return `(${field} IS NULL OR ${field} = '')`; + else return `${field} IS NULL`; + case 'notBlank': + if (type !== 'date') return `${field} IS NOT NULL AND ${field} != ''`; + else return `${field} IS NOT NULL`; + case 'greaterThan': + return `${field} > '${value}'`; + case 'lessThanOrEqual': + return `${field} <= '${value}'`; + case 'lessThan': + return `${field} < '${value}'`; + case 'greaterThan': + return `${field} > '${value}'`; + case 'inRange': + return `${field} >= '${value}' AND ${field} <= '${valueTo}'`; + case 'in': + if (!value || (Array.isArray(value) && value.length === 0)) { + // Return a condition that is always false if value is empty or an empty array + return '1 = 0'; + } + return `${field} IN (${(value as any[]).map((val) => `'${val}'`).join(', ')})`; + default: + return 'true'; // Default return for unmatched conditions + } } function buildLogicalCondition(logicalCondition: LogicalCondition): string { - if (isFieldCondition(logicalCondition)) { - return buildCondition(logicalCondition); + if (isFieldCondition(logicalCondition)) { + return buildCondition(logicalCondition); + } + const parts: string[] = []; + if (logicalCondition.AND && logicalCondition.AND.length > 0) { + const andParts = logicalCondition.AND.map((c) => + buildLogicalCondition(c), + ).filter((part) => part !== ''); // Filter out empty conditions + if (andParts.length > 0) { + parts.push(`(${andParts.join(' AND ')})`); } - const parts: string[] = []; - if (logicalCondition.AND && logicalCondition.AND.length > 0) { - const andParts = logicalCondition.AND - .map(c => buildLogicalCondition(c)) - .filter(part => part !== ''); // Filter out empty conditions - if (andParts.length > 0) { - parts.push(`(${andParts.join(' AND ')})`); - } + } + // Process OR conditions + if (logicalCondition.OR && logicalCondition.OR.length > 0) { + const orParts = logicalCondition.OR.map((c) => + buildLogicalCondition(c), + ).filter((part) => part !== ''); // Filter out empty conditions + if (orParts.length > 0) { + parts.push(`(${orParts.join(' OR ')})`); } - // Process OR conditions - if (logicalCondition.OR && logicalCondition.OR.length > 0) { - const orParts = logicalCondition.OR - .map(c => buildLogicalCondition(c)) - .filter(part => part !== ''); // Filter out empty conditions - if (orParts.length > 0) { - parts.push(`(${orParts.join(' OR ')})`); - } - } - // Join AND and OR parts with an 'AND' if both are present - return parts.length > 1 ? parts.join(' AND ') : parts[0] || ''; + } + // Join AND and OR parts with an 'AND' if both are present + return parts.length > 1 ? parts.join(' AND ') : parts[0] || ''; } export class SQLBuilder { - static select(fields: string[], distinctField?: string): string { - const distinctClause = distinctField ? `DISTINCT ON (${distinctField}) ` : ""; - return `SELECT ${distinctClause}${fields.join(", ")}`; - } - static rowNumber(orderBy: string, partitionBy: string | null = null, alias: string = 'row_num'): string { - if (!orderBy) { - throw new Error("orderBy 参数不能为空"); - } - - let partitionClause = ''; - if (partitionBy) { - partitionClause = `PARTITION BY ${partitionBy} `; - } - - return `ROW_NUMBER() OVER (${partitionClause}ORDER BY ${orderBy}) AS ${alias}`; - } - static from(tableName: string): string { - return `FROM ${tableName}`; + static select(fields: string[], distinctField?: string): string { + const distinctClause = distinctField + ? `DISTINCT ON (${distinctField}) ` + : ''; + return `SELECT ${distinctClause}${fields.join(', ')}`; + } + static rowNumber( + orderBy: string, + partitionBy: string | null = null, + alias: string = 'row_num', + ): string { + if (!orderBy) { + throw new Error('orderBy 参数不能为空'); } - static where(conditions: LogicalCondition): string { - const whereClause = buildLogicalCondition(conditions); - return whereClause ? `WHERE ${whereClause}` : ""; + let partitionClause = ''; + if (partitionBy) { + partitionClause = `PARTITION BY ${partitionBy} `; } - static groupBy(columns: string[]): string { - return columns.length ? `GROUP BY ${columns.join(", ")}` : ""; - } + return `ROW_NUMBER() OVER (${partitionClause}ORDER BY ${orderBy}) AS ${alias}`; + } + static from(tableName: string): string { + return `FROM ${tableName}`; + } - static orderBy(columns: string[]): string { - return columns.length ? `ORDER BY ${columns.join(", ")}` : ""; - } + static where(conditions: LogicalCondition): string { + const whereClause = buildLogicalCondition(conditions); + return whereClause ? `WHERE ${whereClause}` : ''; + } - static limit(pageSize: number, offset: number = 0): string { - return `LIMIT ${pageSize + 1} OFFSET ${offset}`; - } + static groupBy(columns: string[]): string { + return columns.length ? `GROUP BY ${columns.join(', ')}` : ''; + } - static join(clauses: string[]): string { - return clauses.filter(Boolean).join(' '); - } - static createFilterSql(key: string, item: any): LogicalCondition { - const conditionFuncs: Record LogicalCondition> = { - text: (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 }), - set: (item) => ({ value: item.values, op: "in", field: key }) - } - return conditionFuncs[item.filterType](item) + static orderBy(columns: string[]): string { + return columns.length ? `ORDER BY ${columns.join(', ')}` : ''; + } - } + static limit(pageSize: number, offset: number = 0): string { + return `LIMIT ${pageSize + 1} OFFSET ${offset}`; + } + + static join(clauses: string[]): string { + return clauses.filter(Boolean).join(' '); + } + 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 + > = { + text: (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, + }), + set: (item) => ({ value: item.values, op: 'in', field: key }), + }; + return conditionFuncs[item.filterType](item); + } } - diff --git a/apps/server/src/models/rbac/rbac.module.ts b/apps/server/src/models/rbac/rbac.module.ts index 9f3e0f8..fc4e194 100755 --- a/apps/server/src/models/rbac/rbac.module.ts +++ b/apps/server/src/models/rbac/rbac.module.ts @@ -8,7 +8,13 @@ import { DepartmentModule } from '../department/department.module'; @Module({ imports: [DepartmentModule], - providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter], - exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter] + providers: [ + RoleMapService, + RoleRouter, + TrpcService, + RoleService, + RoleMapRouter, + ], + exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter], }) -export class RoleMapModule { } +export class RoleMapModule {} diff --git a/apps/server/src/models/rbac/role.router.ts b/apps/server/src/models/rbac/role.router.ts index 5bb9dd3..ee130a9 100755 --- a/apps/server/src/models/rbac/role.router.ts +++ b/apps/server/src/models/rbac/role.router.ts @@ -3,86 +3,91 @@ import { TrpcService } from '@server/trpc/trpc.service'; import { Prisma, UpdateOrderSchema } from '@nice/common'; import { RoleService } from './role.service'; import { z, ZodType } from 'zod'; -const RoleCreateArgsSchema: ZodType = z.any() -const RoleUpdateArgsSchema: ZodType = z.any() -const RoleCreateManyInputSchema: ZodType = z.any() -const RoleDeleteManyArgsSchema: ZodType = z.any() -const RoleFindManyArgsSchema: ZodType = z.any() -const RoleFindFirstArgsSchema: ZodType = z.any() -const RoleWhereInputSchema: ZodType = z.any() -const RoleSelectSchema: ZodType = z.any() +const RoleCreateArgsSchema: ZodType = z.any(); +const RoleUpdateArgsSchema: ZodType = z.any(); +const RoleCreateManyInputSchema: ZodType = z.any(); +const RoleDeleteManyArgsSchema: ZodType = z.any(); +const RoleFindManyArgsSchema: ZodType = z.any(); +const RoleFindFirstArgsSchema: ZodType = z.any(); +const RoleWhereInputSchema: ZodType = z.any(); +const RoleSelectSchema: ZodType = z.any(); const RoleUpdateInputSchema: ZodType = z.any(); @Injectable() export class RoleRouter { - constructor( - private readonly trpc: TrpcService, - private readonly roleService: RoleService, - ) { } - router = this.trpc.router({ - create: this.trpc.protectProcedure - .input(RoleCreateArgsSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.roleService.create(input, staff); - }), - update: this.trpc.protectProcedure - .input(RoleUpdateArgsSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.roleService.update(input, staff); - }), - createMany: this.trpc.protectProcedure.input(z.array(RoleCreateManyInputSchema)) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; + constructor( + private readonly trpc: TrpcService, + private readonly roleService: RoleService, + ) {} + router = this.trpc.router({ + create: this.trpc.protectProcedure + .input(RoleCreateArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.roleService.create(input, staff); + }), + update: this.trpc.protectProcedure + .input(RoleUpdateArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.roleService.update(input, staff); + }), + createMany: this.trpc.protectProcedure + .input(z.array(RoleCreateManyInputSchema)) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; - return await this.roleService.createMany({ data: input }, staff); - }), - softDeleteByIds: this.trpc.protectProcedure - .input( - z.object({ - ids: z.array(z.string()), - data: RoleUpdateInputSchema.optional() - }), - ) - .mutation(async ({ input }) => { - return await this.roleService.softDeleteByIds(input.ids, input.data); - }), - findFirst: this.trpc.procedure - .input(RoleFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.roleService.findFirst(input); - }), + return await this.roleService.createMany({ data: input }, staff); + }), + softDeleteByIds: this.trpc.protectProcedure + .input( + z.object({ + ids: z.array(z.string()), + data: RoleUpdateInputSchema.optional(), + }), + ) + .mutation(async ({ input }) => { + return await this.roleService.softDeleteByIds(input.ids, input.data); + }), + findFirst: this.trpc.procedure + .input(RoleFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.roleService.findFirst(input); + }), - updateOrder: this.trpc.protectProcedure - .input(UpdateOrderSchema) - .mutation(async ({ input }) => { - return this.roleService.updateOrder(input); - }), - findMany: this.trpc.procedure - .input(RoleFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.roleService.findMany(input); - }), - findManyWithCursor: this.trpc.protectProcedure - .input(z.object({ - cursor: z.any().nullish(), - take: z.number().optional(), - where: RoleWhereInputSchema.optional(), - select: RoleSelectSchema.optional() - })) - .query(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.roleService.findManyWithCursor(input); - }), - findManyWithPagination: this.trpc.procedure - .input(z.object({ - page: z.number(), - pageSize: z.number().optional(), - where: RoleWhereInputSchema.optional(), - select: RoleSelectSchema.optional() - })) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.roleService.findManyWithPagination(input); - }), - }); + updateOrder: this.trpc.protectProcedure + .input(UpdateOrderSchema) + .mutation(async ({ input }) => { + return this.roleService.updateOrder(input); + }), + findMany: this.trpc.procedure + .input(RoleFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.roleService.findMany(input); + }), + findManyWithCursor: this.trpc.protectProcedure + .input( + z.object({ + cursor: z.any().nullish(), + take: z.number().optional(), + where: RoleWhereInputSchema.optional(), + select: RoleSelectSchema.optional(), + }), + ) + .query(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.roleService.findManyWithCursor(input); + }), + findManyWithPagination: this.trpc.procedure + .input( + z.object({ + page: z.number(), + pageSize: z.number().optional(), + where: RoleWhereInputSchema.optional(), + select: RoleSelectSchema.optional(), + }), + ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.roleService.findManyWithPagination(input); + }), + }); } diff --git a/apps/server/src/models/rbac/role.row.service.ts b/apps/server/src/models/rbac/role.row.service.ts index 27e4574..a7111e8 100755 --- a/apps/server/src/models/rbac/role.row.service.ts +++ b/apps/server/src/models/rbac/role.row.service.ts @@ -1,47 +1,59 @@ -import { db, ObjectType, RowModelRequest, RowRequestSchema, UserProfile } from "@nice/common"; -import { RowCacheService } from "../base/row-cache.service"; -import { isFieldCondition, LogicalCondition } from "../base/sql-builder"; -import { z } from "zod"; +import { + db, + ObjectType, + 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 { - protected createGetRowsFilters( - request: z.infer, - staff?: UserProfile - ) { - const condition = super.createGetRowsFilters(request) - if (isFieldCondition(condition)) - return {} - const baseModelCondition: LogicalCondition[] = [{ - field: `${this.tableName}.deleted_at`, - op: "blank", - type: "date" - }] - condition.AND = [...baseModelCondition, ...condition.AND!] - return condition - } - createUnGroupingRowSelect(): string[] { - return [ - `${this.tableName}.id AS id`, - `${this.tableName}.name AS name`, - `${this.tableName}.system AS system`, - `${this.tableName}.permissions AS permissions` - ]; - } - protected async getRowDto(data: any, staff?: UserProfile): Promise { - if (!data.id) - return data - const roleMaps = await db.roleMap.findMany({ - where: { - roleId: data.id - } - }) - const deptIds = roleMaps.filter(item => item.objectType === ObjectType.DEPARTMENT).map(roleMap => roleMap.objectId) - const staffIds = roleMaps.filter(item => item.objectType === ObjectType.STAFF).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[] { - return []; - } -} \ No newline at end of file + protected createGetRowsFilters( + request: z.infer, + staff?: UserProfile, + ) { + const condition = super.createGetRowsFilters(request); + if (isFieldCondition(condition)) return {}; + const baseModelCondition: LogicalCondition[] = [ + { + field: `${this.tableName}.deleted_at`, + op: 'blank', + type: 'date', + }, + ]; + condition.AND = [...baseModelCondition, ...condition.AND!]; + return condition; + } + createUnGroupingRowSelect(): string[] { + return [ + `${this.tableName}.id AS id`, + `${this.tableName}.name AS name`, + `${this.tableName}.system AS system`, + `${this.tableName}.permissions AS permissions`, + ]; + } + protected async getRowDto(data: any, staff?: UserProfile): Promise { + if (!data.id) return data; + const roleMaps = await db.roleMap.findMany({ + where: { + roleId: data.id, + }, + }); + const deptIds = roleMaps + .filter((item) => item.objectType === ObjectType.DEPARTMENT) + .map((roleMap) => roleMap.objectId); + const staffIds = roleMaps + .filter((item) => item.objectType === ObjectType.STAFF) + .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[] { + return []; + } +} diff --git a/apps/server/src/models/rbac/rolemap.router.ts b/apps/server/src/models/rbac/rolemap.router.ts index 72ae5a4..ebed1ac 100755 --- a/apps/server/src/models/rbac/rolemap.router.ts +++ b/apps/server/src/models/rbac/rolemap.router.ts @@ -1,9 +1,6 @@ import { Injectable } from '@nestjs/common'; import { TrpcService } from '@server/trpc/trpc.service'; -import { - ObjectType, - RoleMapMethodSchema, -} from '@nice/common'; +import { ObjectType, RoleMapMethodSchema } from '@nice/common'; import { RoleMapService } from './rolemap.service'; @Injectable() @@ -11,7 +8,7 @@ export class RoleMapRouter { constructor( private readonly trpc: TrpcService, private readonly roleMapService: RoleMapService, - ) { } + ) {} router = this.trpc.router({ deleteAllRolesForObject: this.trpc.protectProcedure .input(RoleMapMethodSchema.deleteWithObject) diff --git a/apps/server/src/models/rbac/rolemap.service.ts b/apps/server/src/models/rbac/rolemap.service.ts index d3c971a..4c3bf73 100755 --- a/apps/server/src/models/rbac/rolemap.service.ts +++ b/apps/server/src/models/rbac/rolemap.service.ts @@ -64,10 +64,7 @@ export class RoleMapService extends RowModelService { return condition; } - protected async getRowDto( - row: any, - staff?: UserProfile, - ): Promise { + protected async getRowDto(row: any, staff?: UserProfile): Promise { if (!row.id) return row; return row; } @@ -126,15 +123,17 @@ export class RoleMapService extends RowModelService { data: roleMaps, }); }); - const wrapResult = Promise.all(result.map(async item => { - const staff = await db.staff.findMany({ - include: { department: true }, - where: { - id: item.objectId - } - }) - return { ...item, staff } - })) + const wrapResult = Promise.all( + result.map(async (item) => { + const staff = await db.staff.findMany({ + include: { department: true }, + where: { + id: item.objectId, + }, + }); + return { ...item, staff }; + }), + ); return wrapResult; } async addRoleForObjects( @@ -187,11 +186,11 @@ export class RoleMapService extends RowModelService { { objectId: staffId, objectType: ObjectType.STAFF }, ...(deptId || ancestorDeptIds.length > 0 ? [ - { - objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) }, - objectType: ObjectType.DEPARTMENT, - }, - ] + { + objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) }, + objectType: ObjectType.DEPARTMENT, + }, + ] : []), ]; // Helper function to fetch roles based on domain ID. @@ -260,7 +259,9 @@ export class RoleMapService extends RowModelService { // const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item))); return { items, totalCount }; } - async getStaffsNotMap(data: z.infer) { + async getStaffsNotMap( + data: z.infer, + ) { const { domainId, roleId } = data; let staffs = await db.staff.findMany({ where: { @@ -300,7 +301,9 @@ export class RoleMapService extends RowModelService { * @param data 包含角色ID和域ID的数据 * @returns 角色映射详情,包含部门ID和员工ID列表 */ - async getRoleMapDetail(data: z.infer) { + async getRoleMapDetail( + data: z.infer, + ) { const { roleId, domainId } = data; const res = await db.roleMap.findMany({ where: { roleId, domainId } }); diff --git a/apps/server/src/models/resource/processor/BaseProcessor.ts b/apps/server/src/models/resource/processor/BaseProcessor.ts index 21907f5..53988d5 100755 --- a/apps/server/src/models/resource/processor/BaseProcessor.ts +++ b/apps/server/src/models/resource/processor/BaseProcessor.ts @@ -1,23 +1,24 @@ -import path, { dirname } from "path"; -import { FileMetadata, VideoMetadata, ResourceProcessor } from "../types"; -import { Resource, ResourceStatus, db } from "@nice/common"; -import { Logger } from "@nestjs/common"; +import path, { dirname } from 'path'; +import { FileMetadata, VideoMetadata, ResourceProcessor } from '../types'; +import { Resource, ResourceStatus, db } from '@nice/common'; +import { Logger } from '@nestjs/common'; import fs from 'fs/promises'; export abstract class BaseProcessor implements ResourceProcessor { - constructor() { } - protected logger = new Logger(BaseProcessor.name) + constructor() {} + protected logger = new Logger(BaseProcessor.name); - abstract process(resource: Resource): Promise - protected createOutputDir(filepath: string, subdirectory: string = 'assets'): 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}`)); - - return outputDir; - - } + abstract process(resource: Resource): Promise; + protected createOutputDir( + filepath: string, + subdirectory: string = 'assets', + ): 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}`), + ); + + return outputDir; + } } -// \ No newline at end of file +// diff --git a/apps/server/src/models/resource/resource.module.ts b/apps/server/src/models/resource/resource.module.ts index 153bc6e..a2b9dbf 100755 --- a/apps/server/src/models/resource/resource.module.ts +++ b/apps/server/src/models/resource/resource.module.ts @@ -4,7 +4,7 @@ import { ResourceService } from './resource.service'; import { TrpcService } from '@server/trpc/trpc.service'; @Module({ - exports: [ResourceRouter, ResourceService], - providers: [ResourceRouter, ResourceService, TrpcService], + exports: [ResourceRouter, ResourceService], + providers: [ResourceRouter, ResourceService, TrpcService], }) -export class ResourceModule { } +export class ResourceModule {} diff --git a/apps/server/src/models/resource/resource.service.ts b/apps/server/src/models/resource/resource.service.ts index 1f2f9a9..332bfe3 100755 --- a/apps/server/src/models/resource/resource.service.ts +++ b/apps/server/src/models/resource/resource.service.ts @@ -1,4 +1,4 @@ -import { Injectable ,Logger} from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { BaseService } from '../base/base.service'; import { diff --git a/apps/server/src/models/resource/types.ts b/apps/server/src/models/resource/types.ts index eb060ba..22de9dc 100755 --- a/apps/server/src/models/resource/types.ts +++ b/apps/server/src/models/resource/types.ts @@ -1,55 +1,57 @@ -import { Resource } from "@nice/common"; +import { Resource } from '@nice/common'; export interface ResourceProcessor { - process(resource: Resource): Promise + process(resource: Resource): Promise; } export interface ProcessResult { - success: boolean - resource: Resource - error?: Error + success: boolean; + resource: Resource; + error?: Error; } export interface BaseMetadata { - size: number - filetype: string - filename: string - extension: string - modifiedAt: Date + size: number; + filetype: string; + filename: string; + extension: string; + modifiedAt: Date; } /** * 图片特有元数据接口 */ export interface ImageMetadata { - width: number; // 图片宽度(px) - height: number; // 图片高度(px) - compressedUrl?: string; - orientation?: number; // EXIF方向信息 - space?: string; // 色彩空间 (如: RGB, CMYK) - hasAlpha?: boolean; // 是否包含透明通道 + width: number; // 图片宽度(px) + height: number; // 图片高度(px) + compressedUrl?: string; + orientation?: number; // EXIF方向信息 + space?: string; // 色彩空间 (如: RGB, CMYK) + hasAlpha?: boolean; // 是否包含透明通道 } /** * 视频特有元数据接口 */ export interface VideoMetadata { - width?: number; - height?: number; - duration?: number; - videoCodec?: string; - audioCodec?: string; - coverUrl?: string + width?: number; + height?: number; + duration?: number; + videoCodec?: string; + audioCodec?: string; + coverUrl?: string; } /** * 音频特有元数据接口 */ export interface AudioMetadata { - duration: number; // 音频时长(秒) - bitrate?: number; // 比特率(bps) - sampleRate?: number; // 采样率(Hz) - channels?: number; // 声道数 - codec?: string; // 音频编码格式 + duration: number; // 音频时长(秒) + bitrate?: number; // 比特率(bps) + sampleRate?: number; // 采样率(Hz) + channels?: number; // 声道数 + codec?: string; // 音频编码格式 } - -export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata \ No newline at end of file +export type FileMetadata = ImageMetadata & + VideoMetadata & + AudioMetadata & + BaseMetadata; diff --git a/apps/server/src/models/staff/staff.module.ts b/apps/server/src/models/staff/staff.module.ts index fa681dc..c9e787f 100755 --- a/apps/server/src/models/staff/staff.module.ts +++ b/apps/server/src/models/staff/staff.module.ts @@ -12,4 +12,4 @@ import { StaffRowService } from './staff.row.service'; exports: [StaffService, StaffRouter, StaffRowService], controllers: [StaffController], }) -export class StaffModule { } +export class StaffModule {} diff --git a/apps/server/src/models/taxonomy/taxonomy.router.ts b/apps/server/src/models/taxonomy/taxonomy.router.ts index e827b36..a2becda 100755 --- a/apps/server/src/models/taxonomy/taxonomy.router.ts +++ b/apps/server/src/models/taxonomy/taxonomy.router.ts @@ -8,7 +8,7 @@ export class TaxonomyRouter { constructor( private readonly trpc: TrpcService, private readonly taxonomyService: TaxonomyService, - ) { } + ) {} router = this.trpc.router({ create: this.trpc.procedure diff --git a/apps/server/src/models/term/term.module.ts b/apps/server/src/models/term/term.module.ts index 850dbc1..bccea04 100755 --- a/apps/server/src/models/term/term.module.ts +++ b/apps/server/src/models/term/term.module.ts @@ -13,4 +13,4 @@ import { TermRowService } from './term.row.service'; exports: [TermService, TermRouter], controllers: [TermController], }) -export class TermModule { } +export class TermModule {} diff --git a/apps/server/src/models/transform/transform.module.ts b/apps/server/src/models/transform/transform.module.ts index e7d2550..f7cc7a8 100755 --- a/apps/server/src/models/transform/transform.module.ts +++ b/apps/server/src/models/transform/transform.module.ts @@ -8,12 +8,7 @@ import { DepartmentModule } from '../department/department.module'; import { StaffModule } from '../staff/staff.module'; // import { TransformController } from './transform.controller'; @Module({ - imports: [ - DepartmentModule, - StaffModule, - TermModule, - TaxonomyModule, - ], + imports: [DepartmentModule, StaffModule, TermModule, TaxonomyModule], providers: [TransformService, TransformRouter, TrpcService], exports: [TransformRouter, TransformService], // controllers:[TransformController] diff --git a/apps/server/src/socket/base/base-websocket-server.ts b/apps/server/src/socket/base/base-websocket-server.ts index f5716c1..93547de 100755 --- a/apps/server/src/socket/base/base-websocket-server.ts +++ b/apps/server/src/socket/base/base-websocket-server.ts @@ -1,205 +1,210 @@ - -import { WebSocketServer, WebSocket } from "ws"; -import { Logger } from "@nestjs/common"; -import { WebSocketServerConfig, WSClient, WebSocketType } from "../types"; +import { WebSocketServer, WebSocket } from 'ws'; +import { Logger } from '@nestjs/common'; +import { WebSocketServerConfig, WSClient, WebSocketType } from '../types'; import { SocketMessage } from '@nice/common'; const DEFAULT_CONFIG: WebSocketServerConfig = { - pingInterval: 30000, - pingTimeout: 5000, - debug: false, // 新增默认调试配置 + pingInterval: 30000, + pingTimeout: 5000, + debug: false, // 新增默认调试配置 }; interface IWebSocketServer { - start(): Promise; - stop(): Promise; - broadcast(data: any): void; - handleConnection(ws: WSClient): void; - handleDisconnection(ws: WSClient): void; + start(): Promise; + stop(): Promise; + broadcast(data: any): void; + handleConnection(ws: WSClient): void; + handleDisconnection(ws: WSClient): void; } export abstract class BaseWebSocketServer implements IWebSocketServer { - private _wss: WebSocketServer | null = null; - protected clients: Set = new Set(); - protected timeouts: Map = new Map(); - protected pingIntervalId?: NodeJS.Timeout; - protected readonly logger = new Logger(this.constructor.name); - protected readonly finalConfig: WebSocketServerConfig; - private userClientMap: Map = new Map(); - constructor( - protected readonly config: Partial = {} - ) { - this.finalConfig = { - ...DEFAULT_CONFIG, - ...config, - }; + private _wss: WebSocketServer | null = null; + protected clients: Set = new Set(); + protected timeouts: Map = new Map(); + protected pingIntervalId?: NodeJS.Timeout; + protected readonly logger = new Logger(this.constructor.name); + protected readonly finalConfig: WebSocketServerConfig; + private userClientMap: Map = new Map(); + constructor(protected readonly config: Partial = {}) { + this.finalConfig = { + ...DEFAULT_CONFIG, + ...config, + }; + } + protected debugLog(message: string, ...optionalParams: any[]): void { + if (this.finalConfig.debug) { + this.logger.debug(message, ...optionalParams); } - protected debugLog(message: string, ...optionalParams: any[]): void { - if (this.finalConfig.debug) { - this.logger.debug(message, ...optionalParams); - } - } - public getClientCount() { - return this.clients.size - } - // 暴露 WebSocketServer 实例的只读访问 - public get wss(): WebSocketServer | null { - return this._wss; + } + public getClientCount() { + return this.clients.size; + } + // 暴露 WebSocketServer 实例的只读访问 + public get wss(): WebSocketServer | null { + return this._wss; + } + + // 内部使用的 setter + protected set wss(value: WebSocketServer | null) { + this._wss = value; + } + + public abstract get serverType(): WebSocketType; + + public get serverPath(): string { + return this.finalConfig.path || `/${this.serverType}`; + } + + public async start(): Promise { + if (this._wss) await this.stop(); + + this._wss = new WebSocketServer({ + noServer: true, + path: this.serverPath, + }); + + this.debugLog(`WebSocket server starting on path: ${this.serverPath}`); + this.setupServerEvents(); + this.startPingInterval(); + } + + public async stop(): Promise { + if (this.pingIntervalId) { + clearInterval(this.pingIntervalId); + this.pingIntervalId = undefined; } - // 内部使用的 setter - protected set wss(value: WebSocketServer | null) { - this._wss = value; + this.clients.forEach((client) => client.close()); + this.clients.clear(); + this.timeouts.clear(); + + if (this._wss) { + await new Promise((resolve) => this._wss!.close(resolve)); + this._wss = null; } - public abstract get serverType(): WebSocketType; + this.debugLog(`WebSocket server stopped on path: ${this.serverPath}`); + } - public get serverPath(): string { - return this.finalConfig.path || `/${this.serverType}`; + public broadcast(data: SocketMessage): void { + this.clients.forEach( + (client) => + client.readyState === WebSocket.OPEN && + client.send(JSON.stringify(data)), + ); + } + public sendToUser(id: string, data: SocketMessage) { + const message = JSON.stringify(data); + const client = this.userClientMap.get(id); + client?.send(message); + } + public sendToUsers(ids: string[], data: SocketMessage) { + const message = JSON.stringify(data); + ids.forEach((id) => { + const client = this.userClientMap.get(id); + client?.send(message); + }); + } + public sendToRoom(roomId: string, data: SocketMessage) { + const message = JSON.stringify(data); + this.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN && client.roomId === roomId) { + client.send(message); + } + }); + } + protected getRoomClientsCount(roomId?: string): number { + if (!roomId) return 0; + return Array.from(this.clients).filter((client) => client.roomId === roomId) + .length; + } + + public handleConnection(ws: WSClient): void { + if (ws.userId) { + this.userClientMap.set(ws.userId, ws); } + ws.isAlive = true; + ws.type = this.serverType; + this.clients.add(ws); + this.setupClientEvents(ws); - public async start(): Promise { - if (this._wss) await this.stop(); - - this._wss = new WebSocketServer({ - noServer: true, - path: this.serverPath - }); - - this.debugLog(`WebSocket server starting on path: ${this.serverPath}`); - this.setupServerEvents(); - this.startPingInterval(); - } - - public async stop(): Promise { - if (this.pingIntervalId) { - clearInterval(this.pingIntervalId); - this.pingIntervalId = undefined; - } - - this.clients.forEach(client => client.close()); - this.clients.clear(); - this.timeouts.clear(); - - if (this._wss) { - await new Promise(resolve => this._wss!.close(resolve)); - this._wss = null; - } - - this.debugLog(`WebSocket server stopped on path: ${this.serverPath}`); - } - - public broadcast(data: SocketMessage): void { - this.clients.forEach(client => - client.readyState === WebSocket.OPEN && client.send(JSON.stringify(data)) - ); - } - public sendToUser(id: string, data: SocketMessage) { - const message = JSON.stringify(data); - const client = this.userClientMap.get(id); - client?.send(message) - } - public sendToUsers(ids: string[], data: SocketMessage) { - const message = JSON.stringify(data); - ids.forEach(id => { - const client = this.userClientMap.get(id); - client?.send(message); - }); - } - public sendToRoom(roomId: string, data: SocketMessage) { - const message = JSON.stringify(data); - this.clients.forEach(client => { - if (client.readyState === WebSocket.OPEN && client.roomId === roomId) { - client.send(message) - } - }) - } - protected getRoomClientsCount(roomId?: string): number { - if (!roomId) return 0; - return Array.from(this.clients).filter(client => client.roomId === roomId).length; - } - - public handleConnection(ws: WSClient): void { - if (ws.userId) { - this.userClientMap.set(ws.userId, ws); - } - ws.isAlive = true; - ws.type = this.serverType; - this.clients.add(ws); - this.setupClientEvents(ws); - - const roomClientsCount = this.getRoomClientsCount(ws.roomId); - this.debugLog(` + const roomClientsCount = this.getRoomClientsCount(ws.roomId); + this.debugLog(` [${this.serverType}] connected userId ${ws.userId} roomId ${ws.roomId} room clients ${roomClientsCount} total clients ${this.clients.size}`); + } + + public handleDisconnection(ws: WSClient): void { + if (ws.userId) { + this.userClientMap.delete(ws.userId); } + this.clients.delete(ws); + const timeout = this.timeouts.get(ws); + if (timeout) { + clearTimeout(timeout); + this.timeouts.delete(ws); + } + ws.terminate(); - public handleDisconnection(ws: WSClient): void { - if (ws.userId) { - this.userClientMap.delete(ws.userId); - } - this.clients.delete(ws); - const timeout = this.timeouts.get(ws); - if (timeout) { - clearTimeout(timeout); - this.timeouts.delete(ws); - } - ws.terminate(); + const roomClientsCount = this.getRoomClientsCount(ws.roomId); - const roomClientsCount = this.getRoomClientsCount(ws.roomId); - - this.debugLog(` + this.debugLog(` [${this.serverType}] disconnected userId ${ws.userId} roomId ${ws.roomId} room clients ${roomClientsCount} total clients ${this.clients.size}`); - } - protected setupClientEvents(ws: WSClient): void { - ws.on('pong', () => this.handlePong(ws)) - .on('close', () => this.handleDisconnection(ws)) - .on('error', (error) => { - this.logger.error(`[${this.serverType}] client error on path ${this.serverPath}:`, error); - this.handleDisconnection(ws); - }); - } - - private handlePong(ws: WSClient): void { - ws.isAlive = true; - const timeout = this.timeouts.get(ws); - if (timeout) { - clearTimeout(timeout); - this.timeouts.delete(ws); - } - } - - private startPingInterval(): void { - this.pingIntervalId = setInterval( - () => this.pingClients(), - this.finalConfig.pingInterval + } + protected setupClientEvents(ws: WSClient): void { + ws.on('pong', () => this.handlePong(ws)) + .on('close', () => this.handleDisconnection(ws)) + .on('error', (error) => { + this.logger.error( + `[${this.serverType}] client error on path ${this.serverPath}:`, + error, ); - } + this.handleDisconnection(ws); + }); + } - private pingClients(): void { - this.clients.forEach(ws => { - if (!ws.isAlive) return this.handleDisconnection(ws); - - ws.isAlive = false; - ws.ping(); - const timeout = setTimeout( - () => !ws.isAlive && this.handleDisconnection(ws), - this.finalConfig.pingTimeout - ); - this.timeouts.set(ws, timeout); - }); + private handlePong(ws: WSClient): void { + ws.isAlive = true; + const timeout = this.timeouts.get(ws); + if (timeout) { + clearTimeout(timeout); + this.timeouts.delete(ws); } + } - protected setupServerEvents(): void { - if (!this._wss) return; - this._wss - .on('connection', (ws: WSClient) => this.handleConnection(ws)) - .on('error', (error) => this.logger.error(`Server error on path ${this.serverPath}:`, error)); - } + private startPingInterval(): void { + this.pingIntervalId = setInterval( + () => this.pingClients(), + this.finalConfig.pingInterval, + ); + } + + private pingClients(): void { + this.clients.forEach((ws) => { + if (!ws.isAlive) return this.handleDisconnection(ws); + + ws.isAlive = false; + ws.ping(); + const timeout = setTimeout( + () => !ws.isAlive && this.handleDisconnection(ws), + this.finalConfig.pingTimeout, + ); + this.timeouts.set(ws, timeout); + }); + } + + protected setupServerEvents(): void { + if (!this._wss) return; + this._wss + .on('connection', (ws: WSClient) => this.handleConnection(ws)) + .on('error', (error) => + this.logger.error(`Server error on path ${this.serverPath}:`, error), + ); + } } diff --git a/apps/server/src/socket/collaboration/callback.ts b/apps/server/src/socket/collaboration/callback.ts index a8942f0..77bf4f5 100755 --- a/apps/server/src/socket/collaboration/callback.ts +++ b/apps/server/src/socket/collaboration/callback.ts @@ -8,12 +8,13 @@ import http from 'http'; import { parseInt as libParseInt } from 'lib0/number'; import { WSSharedDoc } from './ws-shared-doc'; - /** * 回调URL配置,从环境变量中获取 * 如果环境变量未设置则为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格式的配置 */ -const CALLBACK_OBJECTS: Record = process.env.CALLBACK_OBJECTS ? JSON.parse(process.env.CALLBACK_OBJECTS) : {}; +const CALLBACK_OBJECTS: Record = process.env.CALLBACK_OBJECTS + ? JSON.parse(process.env.CALLBACK_OBJECTS) + : {}; /** * 导出回调URL是否已配置的标志 @@ -37,10 +40,13 @@ export const isCallbackSet = !!CALLBACK_URL; */ interface DataToSend { room: string; // 房间/文档标识 - data: Record; + data: Record< + string, + { + type: string; // 数据类型 + content: any; // 数据内容 + } + >; } /** @@ -59,25 +65,29 @@ type OriginType = any; * @param origin - 更新的来源 * @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 dataToSend: DataToSend = { room, - data: {} + data: {}, }; // 获取所有需要监听的共享对象名称 const sharedObjectList = Object.keys(CALLBACK_OBJECTS); - + // 遍历所有共享对象,获取它们的最新内容 - sharedObjectList.forEach(sharedObjectName => { + sharedObjectList.forEach((sharedObjectName) => { const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName]; dataToSend.data[sharedObjectName] = { 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', headers: { 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(dataString) - } + 'Content-Length': Buffer.byteLength(dataString), + }, }; // 创建HTTP请求 @@ -137,14 +147,24 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => { * @param doc - 共享文档实例 * @returns 共享对象的内容 */ -const getContent = (objName: string, objType: string, doc: WSSharedDoc): any => { +const getContent = ( + objName: string, + objType: string, + doc: WSSharedDoc, +): any => { // 根据对象类型返回相应的共享对象 switch (objType) { - case 'Array': return doc.getArray(objName); - case 'Map': return doc.getMap(objName); - case 'Text': return doc.getText(objName); - case 'XmlFragment': return doc.getXmlFragment(objName); - case 'XmlElement': return doc.getXmlElement(objName); - default: return {}; + case 'Array': + return doc.getArray(objName); + case 'Map': + return doc.getMap(objName); + case 'Text': + return doc.getText(objName); + case 'XmlFragment': + return doc.getXmlFragment(objName); + case 'XmlElement': + return doc.getXmlElement(objName); + default: + return {}; } }; diff --git a/apps/server/src/socket/collaboration/collaboration.module.ts b/apps/server/src/socket/collaboration/collaboration.module.ts index 71f99ea..0ccf6a4 100755 --- a/apps/server/src/socket/collaboration/collaboration.module.ts +++ b/apps/server/src/socket/collaboration/collaboration.module.ts @@ -3,6 +3,6 @@ import { YjsServer } from './yjs.server'; @Module({ providers: [YjsServer], - exports: [YjsServer] + exports: [YjsServer], }) -export class CollaborationModule { } +export class CollaborationModule {} diff --git a/apps/server/src/socket/collaboration/persistence.ts b/apps/server/src/socket/collaboration/persistence.ts index 5cdc19b..3a46787 100755 --- a/apps/server/src/socket/collaboration/persistence.ts +++ b/apps/server/src/socket/collaboration/persistence.ts @@ -23,7 +23,7 @@ if (typeof persistenceDir === 'string') { ldb.storeUpdate(docName, update); }); }, - writeState: async (_docName, _ydoc) => { }, + writeState: async (_docName, _ydoc) => {}, }; } diff --git a/apps/server/src/socket/collaboration/types.ts b/apps/server/src/socket/collaboration/types.ts index 502a42f..9588759 100755 --- a/apps/server/src/socket/collaboration/types.ts +++ b/apps/server/src/socket/collaboration/types.ts @@ -1,5 +1,4 @@ export interface ConnectionOptions { - docName: string; - gc: boolean; - } - \ No newline at end of file + docName: string; + gc: boolean; +} diff --git a/apps/server/src/socket/collaboration/ws-shared-doc.ts b/apps/server/src/socket/collaboration/ws-shared-doc.ts index ae1bd09..9caedf2 100755 --- a/apps/server/src/socket/collaboration/ws-shared-doc.ts +++ b/apps/server/src/socket/collaboration/ws-shared-doc.ts @@ -1,158 +1,187 @@ 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 decoding from 'lib0/decoding'; -import * as Y from "yjs" +import * as Y from 'yjs'; import { debounce } from 'lodash'; import { getPersistence, setPersistence } from './persistence'; import { callbackHandler, isCallbackSet } from './callback'; -import { WebSocket } from "ws"; +import { WebSocket } from 'ws'; import { YMessageType } from '@nice/common'; import { WSClient } from '../types'; export const docs = new Map(); -export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000'); -export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000'); +export const CALLBACK_DEBOUNCE_WAIT = parseInt( + 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 => { - return docs.get(docname) || createYDoc(docname, gc); + return docs.get(docname) || createYDoc(docname, gc); }; const createYDoc = (docname: string, gc: boolean): WSSharedDoc => { - const doc = new WSSharedDoc(docname, gc); - docs.set(docname, doc); - return doc; + const doc = new WSSharedDoc(docname, gc); + docs.set(docname, doc); + return doc; }; export const send = (doc: WSSharedDoc, conn: WebSocket, m: Uint8Array) => { - if (conn.readyState !== WebSocket.OPEN) { - closeConn(doc, conn); - return; - } - try { - conn.send(m, {}, err => { err != null && closeConn(doc, conn) }); - } catch (e) { - closeConn(doc, conn); - } + if (conn.readyState !== WebSocket.OPEN) { + closeConn(doc, conn); + return; + } + try { + conn.send(m, {}, (err) => { + err != null && closeConn(doc, conn); + }); + } catch (e) { + closeConn(doc, conn); + } }; export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => { - if (doc.conns.has(conn)) { - const controlledIds = doc.conns.get(conn) as Set; - doc.conns.delete(conn); - removeAwarenessStates( - doc.awareness, - Array.from(controlledIds), - null - ); + if (doc.conns.has(conn)) { + const controlledIds = doc.conns.get(conn) as Set; + doc.conns.delete(conn); + removeAwarenessStates(doc.awareness, Array.from(controlledIds), null); - if (doc.conns.size === 0 && getPersistence() !== null) { - getPersistence()?.writeState(doc.name, doc).then(() => { - doc.destroy(); - }); - docs.delete(doc.name); - } + if (doc.conns.size === 0 && getPersistence() !== null) { + getPersistence() + ?.writeState(doc.name, doc) + .then(() => { + doc.destroy(); + }); + docs.delete(doc.name); } - conn.close(); + } + conn.close(); }; -export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8Array) => { - try { - const encoder = encoding.createEncoder(); - const decoder = decoding.createDecoder(message); - const messageType = decoding.readVarUint(decoder); - switch (messageType) { - case YMessageType.Sync: - // console.log(`received sync message ${message.length}`) - encoding.writeVarUint(encoder, YMessageType.Sync); - readSyncMessage(decoder, encoder, doc, conn); - if (encoding.length(encoder) > 1) { - send(doc, conn, encoding.toUint8Array(encoder)); - } - break; - - case YMessageType.Awareness: { - applyAwarenessUpdate( - doc.awareness, - decoding.readVarUint8Array(decoder), - conn - ); - // console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`) - break; - } - } - } catch (err) { - console.error(err); - doc.emit('error' as any, [err]); - } -}; - -const updateHandler = (update: Uint8Array, _origin: any, doc: WSSharedDoc, _tr: any) => { +export const messageListener = ( + conn: WSClient, + doc: WSSharedDoc, + message: Uint8Array, +) => { + try { const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, YMessageType.Sync); - writeUpdate(encoder, update); - const message = encoding.toUint8Array(encoder); - doc.conns.forEach((_, conn) => send(doc, conn, message)); + const decoder = decoding.createDecoder(message); + const messageType = decoding.readVarUint(decoder); + switch (messageType) { + case YMessageType.Sync: + // console.log(`received sync message ${message.length}`) + encoding.writeVarUint(encoder, YMessageType.Sync); + readSyncMessage(decoder, encoder, doc, conn); + if (encoding.length(encoder) > 1) { + send(doc, conn, encoding.toUint8Array(encoder)); + } + break; + + case YMessageType.Awareness: { + applyAwarenessUpdate( + doc.awareness, + decoding.readVarUint8Array(decoder), + conn, + ); + // console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`) + break; + } + } + } catch (err) { + console.error(err); + doc.emit('error' as any, [err]); + } }; -let contentInitializor: (ydoc: Y.Doc) => Promise = (_ydoc) => Promise.resolve(); +const updateHandler = ( + update: Uint8Array, + _origin: any, + doc: WSSharedDoc, + _tr: any, +) => { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, YMessageType.Sync); + writeUpdate(encoder, update); + const message = encoding.toUint8Array(encoder); + doc.conns.forEach((_, conn) => send(doc, conn, message)); +}; + +let contentInitializor: (ydoc: Y.Doc) => Promise = (_ydoc) => + Promise.resolve(); export const setContentInitializor = (f: (ydoc: Y.Doc) => Promise) => { - contentInitializor = f; + contentInitializor = f; }; export class WSSharedDoc extends Y.Doc { - name: string; - conns: Map>; - awareness: Awareness; - whenInitialized: Promise; + name: string; + conns: Map>; + awareness: Awareness; + whenInitialized: Promise; - constructor(name: string, gc: boolean) { - super({ gc }); + constructor(name: string, gc: boolean) { + super({ gc }); - this.name = name; - this.conns = new Map(); - this.awareness = new Awareness(this); - this.awareness.setLocalState(null); + this.name = name; + this.conns = new Map(); + this.awareness = new Awareness(this); + this.awareness.setLocalState(null); - const awarenessUpdateHandler = ({ - added, - updated, - removed - }: { - added: number[], - updated: number[], - removed: number[] - }, conn: WebSocket) => { - const changedClients = added.concat(updated, removed); - if (changedClients.length === 0) return - if (conn !== null) { - const connControlledIDs = this.conns.get(conn) as Set; - if (connControlledIDs !== undefined) { - added.forEach(clientID => { connControlledIDs.add(clientID); }); - removed.forEach(clientID => { connControlledIDs.delete(clientID); }); - } - } - - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, YMessageType.Awareness); - encoding.writeVarUint8Array( - encoder, - encodeAwarenessUpdate(this.awareness, changedClients) - ); - const buff = encoding.toUint8Array(encoder); - - this.conns.forEach((_, c) => { - send(this, c, buff); - }); - }; - - this.awareness.on('update', awarenessUpdateHandler); - this.on('update', updateHandler as any); - - if (isCallbackSet) { - this.on('update', debounce( - callbackHandler as any, - CALLBACK_DEBOUNCE_WAIT, - { maxWait: CALLBACK_DEBOUNCE_MAXWAIT } - ) as any); + const awarenessUpdateHandler = ( + { + added, + updated, + removed, + }: { + added: number[]; + updated: number[]; + removed: number[]; + }, + conn: WebSocket, + ) => { + const changedClients = added.concat(updated, removed); + if (changedClients.length === 0) return; + if (conn !== null) { + const connControlledIDs = this.conns.get(conn) as Set; + if (connControlledIDs !== undefined) { + added.forEach((clientID) => { + connControlledIDs.add(clientID); + }); + removed.forEach((clientID) => { + connControlledIDs.delete(clientID); + }); } + } - this.whenInitialized = contentInitializor(this); + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, YMessageType.Awareness); + encoding.writeVarUint8Array( + encoder, + encodeAwarenessUpdate(this.awareness, changedClients), + ); + const buff = encoding.toUint8Array(encoder); + + this.conns.forEach((_, c) => { + send(this, c, buff); + }); + }; + + this.awareness.on('update', awarenessUpdateHandler); + this.on('update', updateHandler as any); + + if (isCallbackSet) { + this.on( + 'update', + debounce(callbackHandler as any, CALLBACK_DEBOUNCE_WAIT, { + maxWait: CALLBACK_DEBOUNCE_MAXWAIT, + }) as any, + ); } + + this.whenInitialized = contentInitializor(this); + } } diff --git a/apps/server/src/socket/collaboration/yjs.server.ts b/apps/server/src/socket/collaboration/yjs.server.ts index 0b747bd..e2a2387 100755 --- a/apps/server/src/socket/collaboration/yjs.server.ts +++ b/apps/server/src/socket/collaboration/yjs.server.ts @@ -1,85 +1,117 @@ -import { Injectable } from "@nestjs/common"; -import { WebSocketType, WSClient } from "../types"; -import { BaseWebSocketServer } from "../base/base-websocket-server"; -import { encoding } from "lib0"; -import { YMessageType, writeSyncStep1, encodeAwarenessUpdate } from "@nice/common"; -import { getYDoc, closeConn, WSSharedDoc, messageListener, send } from "./ws-shared-doc"; +import { Injectable } from '@nestjs/common'; +import { WebSocketType, WSClient } from '../types'; +import { BaseWebSocketServer } from '../base/base-websocket-server'; +import { encoding } from 'lib0'; +import { + YMessageType, + writeSyncStep1, + encodeAwarenessUpdate, +} from '@nice/common'; +import { + getYDoc, + closeConn, + WSSharedDoc, + messageListener, + send, +} from './ws-shared-doc'; @Injectable() export class YjsServer extends BaseWebSocketServer { - public get serverType(): WebSocketType { - return WebSocketType.YJS; - } - public override handleConnection( - connection: WSClient - ): void { - super.handleConnection(connection) - try { - connection.binaryType = 'arraybuffer'; - const doc = this.initializeDocument(connection, connection.roomId, true); - this.setupConnectionHandlers(connection, doc); - this.sendInitialSync(connection, doc); - } catch (error: any) { - this.logger.error(`Error in handleNewConnection: ${error.message}`, error.stack); - connection.close(); - } + public get serverType(): WebSocketType { + return WebSocketType.YJS; + } + public override handleConnection(connection: WSClient): void { + super.handleConnection(connection); + try { + connection.binaryType = 'arraybuffer'; + const doc = this.initializeDocument(connection, connection.roomId, true); + this.setupConnectionHandlers(connection, doc); + this.sendInitialSync(connection, doc); + } catch (error: any) { + this.logger.error( + `Error in handleNewConnection: ${error.message}`, + error.stack, + ); + connection.close(); } + } - private initializeDocument(conn: WSClient, docName: string, gc: boolean) { - const doc = getYDoc(docName, gc); + private initializeDocument(conn: WSClient, docName: string, gc: boolean) { + const doc = getYDoc(docName, gc); - doc.conns.set(conn, new Set()); - return doc; - } + doc.conns.set(conn, new Set()); + return doc; + } - private setupConnectionHandlers(connection: WSClient, doc: WSSharedDoc): void { - connection.on('message', (message: ArrayBuffer) => { - this.handleMessage(connection, doc, message); - }); - connection.on('close', () => { - this.handleClose(doc, connection); - }); - connection.on('error', (error) => { - this.logger.error(`WebSocket error for doc ${doc.name}: ${error.message}`, error.stack); - closeConn(doc, connection); - this.logger.warn(`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`); - }); - } + private setupConnectionHandlers( + connection: WSClient, + doc: WSSharedDoc, + ): void { + connection.on('message', (message: ArrayBuffer) => { + this.handleMessage(connection, doc, message); + }); + connection.on('close', () => { + this.handleClose(doc, connection); + }); + connection.on('error', (error) => { + this.logger.error( + `WebSocket error for doc ${doc.name}: ${error.message}`, + error.stack, + ); + closeConn(doc, connection); + this.logger.warn( + `Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`, + ); + }); + } - private handleClose(doc: WSSharedDoc, connection: WSClient): void { - try { - closeConn(doc, connection); - } catch (error: any) { - this.logger.error(`Error closing connection: ${error.message}`, error.stack); - } + private handleClose(doc: WSSharedDoc, connection: WSClient): void { + try { + closeConn(doc, connection); + } catch (error: any) { + this.logger.error( + `Error closing connection: ${error.message}`, + error.stack, + ); } - private handleMessage(connection: WSClient, doc: WSSharedDoc, message: ArrayBuffer): void { - try { - messageListener(connection, doc, new Uint8Array(message)); - } catch (error: any) { - this.logger.error(`Error handling message: ${error.message}`, error.stack); - } + } + private handleMessage( + connection: WSClient, + doc: WSSharedDoc, + message: ArrayBuffer, + ): void { + try { + messageListener(connection, doc, new Uint8Array(message)); + } catch (error: any) { + this.logger.error( + `Error handling message: ${error.message}`, + error.stack, + ); } - private sendInitialSync(connection: WSClient, doc: any): void { - this.sendSyncStep1(connection, doc); - this.sendAwarenessStates(connection, doc); - } - private sendSyncStep1(connection: WSClient, doc: any): void { - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, YMessageType.Sync); - writeSyncStep1(encoder, doc); - send(doc, connection, encoding.toUint8Array(encoder)); - } - private sendAwarenessStates(connection: WSClient, doc: WSSharedDoc): void { - const awarenessStates = doc.awareness.getStates(); + } + private sendInitialSync(connection: WSClient, doc: any): void { + this.sendSyncStep1(connection, doc); + this.sendAwarenessStates(connection, doc); + } + private sendSyncStep1(connection: WSClient, doc: any): void { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, YMessageType.Sync); + writeSyncStep1(encoder, doc); + send(doc, connection, encoding.toUint8Array(encoder)); + } + private sendAwarenessStates(connection: WSClient, doc: WSSharedDoc): void { + const awarenessStates = doc.awareness.getStates(); - if (awarenessStates.size > 0) { - const encoder = encoding.createEncoder(); - encoding.writeVarUint(encoder, YMessageType.Awareness); - encoding.writeVarUint8Array( - encoder, - encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())) - ); - send(doc, connection, encoding.toUint8Array(encoder)); - } + if (awarenessStates.size > 0) { + const encoder = encoding.createEncoder(); + encoding.writeVarUint(encoder, YMessageType.Awareness); + encoding.writeVarUint8Array( + encoder, + encodeAwarenessUpdate( + doc.awareness, + Array.from(awarenessStates.keys()), + ), + ); + send(doc, connection, encoding.toUint8Array(encoder)); } + } } diff --git a/apps/server/src/socket/realtime/realtime.module.ts b/apps/server/src/socket/realtime/realtime.module.ts index 7a5a76e..4d856de 100755 --- a/apps/server/src/socket/realtime/realtime.module.ts +++ b/apps/server/src/socket/realtime/realtime.module.ts @@ -1,9 +1,8 @@ import { Module } from '@nestjs/common'; import { RealtimeServer } from './realtime.server'; - @Module({ - providers: [ RealtimeServer], - exports: [ RealtimeServer] + providers: [RealtimeServer], + exports: [RealtimeServer], }) -export class RealTimeModule { } +export class RealTimeModule {} diff --git a/apps/server/src/socket/realtime/realtime.server.ts b/apps/server/src/socket/realtime/realtime.server.ts index e9dd560..4d4317e 100755 --- a/apps/server/src/socket/realtime/realtime.server.ts +++ b/apps/server/src/socket/realtime/realtime.server.ts @@ -1,25 +1,25 @@ -import { Injectable, OnModuleInit } from "@nestjs/common"; -import { WebSocketType } from "../types"; -import { BaseWebSocketServer } from "../base/base-websocket-server"; -import EventBus, { CrudOperation } from "@server/utils/event-bus"; -import { ObjectType, SocketMsgType } from "@nice/common"; +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { WebSocketType } from '../types'; +import { BaseWebSocketServer } from '../base/base-websocket-server'; +import EventBus, { CrudOperation } from '@server/utils/event-bus'; +import { ObjectType, SocketMsgType } from '@nice/common'; @Injectable() -export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit { - onModuleInit() { - EventBus.on("dataChanged", ({ data, type, operation }) => { - // if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) { - // const receiverIds = (data as Partial).receivers.map(receiver => receiver.id) - // this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } }) - // } - - // if (type === ObjectType.POST) { - // const post = data as Partial - - // } - }) - - } - public get serverType(): WebSocketType { - return WebSocketType.REALTIME; - } +export class RealtimeServer + extends BaseWebSocketServer + implements OnModuleInit +{ + onModuleInit() { + EventBus.on('dataChanged', ({ data, type, operation }) => { + // if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) { + // const receiverIds = (data as Partial).receivers.map(receiver => receiver.id) + // this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } }) + // } + // if (type === ObjectType.POST) { + // const post = data as Partial + // } + }); + } + public get serverType(): WebSocketType { + return WebSocketType.REALTIME; + } } diff --git a/apps/server/src/socket/types.ts b/apps/server/src/socket/types.ts index 01b689d..33e9dbd 100755 --- a/apps/server/src/socket/types.ts +++ b/apps/server/src/socket/types.ts @@ -1,29 +1,29 @@ -import { WebSocketServer, WebSocket } from "ws"; +import { WebSocketServer, WebSocket } from 'ws'; // 类型定义 export enum WebSocketType { - YJS = "yjs", - REALTIME = "realtime" + YJS = 'yjs', + REALTIME = 'realtime', } export interface WebSocketServerConfig { - path?: string; - pingInterval?: number; - pingTimeout?: number; - debug?: boolean + path?: string; + pingInterval?: number; + pingTimeout?: number; + debug?: boolean; } export interface ServerInstance { - wss: WebSocketServer | null; - clients: Set; - pingIntervalId?: NodeJS.Timeout; - timeouts: Map; + wss: WebSocketServer | null; + clients: Set; + pingIntervalId?: NodeJS.Timeout; + timeouts: Map; } export interface WSClient extends WebSocket { - isAlive?: boolean; - type?: WebSocketType; - userId?: string - origin?: string - roomId?: string -} \ No newline at end of file + isAlive?: boolean; + type?: WebSocketType; + userId?: string; + origin?: string; + roomId?: string; +} diff --git a/apps/server/src/socket/websocket.module.ts b/apps/server/src/socket/websocket.module.ts index 050fc0d..a17c391 100755 --- a/apps/server/src/socket/websocket.module.ts +++ b/apps/server/src/socket/websocket.module.ts @@ -8,4 +8,4 @@ import { CollaborationModule } from './collaboration/collaboration.module'; providers: [WebSocketService], exports: [WebSocketService], }) -export class WebSocketModule { } +export class WebSocketModule {} diff --git a/apps/server/src/socket/websocket.service.ts b/apps/server/src/socket/websocket.service.ts index 1a110a3..c0a33f9 100755 --- a/apps/server/src/socket/websocket.service.ts +++ b/apps/server/src/socket/websocket.service.ts @@ -1,61 +1,61 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { Server } from "http"; -import { WSClient } from "./types"; -import { RealtimeServer } from "./realtime/realtime.server"; -import { YjsServer } from "./collaboration/yjs.server"; -import { BaseWebSocketServer } from "./base/base-websocket-server"; +import { Injectable, Logger } from '@nestjs/common'; +import { Server } from 'http'; +import { WSClient } from './types'; +import { RealtimeServer } from './realtime/realtime.server'; +import { YjsServer } from './collaboration/yjs.server'; +import { BaseWebSocketServer } from './base/base-websocket-server'; @Injectable() export class WebSocketService { - private readonly logger = new Logger(WebSocketService.name); - private readonly servers: BaseWebSocketServer[] = []; - constructor( - private realTimeServer: RealtimeServer, - private yjsServer: YjsServer - ) { - this.servers.push(this.realTimeServer) - this.servers.push(this.yjsServer) + private readonly logger = new Logger(WebSocketService.name); + private readonly servers: BaseWebSocketServer[] = []; + constructor( + private realTimeServer: RealtimeServer, + private yjsServer: YjsServer, + ) { + this.servers.push(this.realTimeServer); + this.servers.push(this.yjsServer); + } + public async initialize(httpServer: Server): Promise { + try { + await Promise.all(this.servers.map((server) => server.start())); + this.setupUpgradeHandler(httpServer); + } catch (error) { + this.logger.error('Failed to initialize:', error); + throw error; } - public async initialize(httpServer: Server): Promise { - try { - await Promise.all(this.servers.map(server => server.start())); - this.setupUpgradeHandler(httpServer); - } catch (error) { - this.logger.error('Failed to initialize:', error); - throw error; - } - } - private setupUpgradeHandler(httpServer: Server): void { - if (httpServer.listeners('upgrade').length) return; - httpServer.on('upgrade', async (request, socket, head) => { - try { - const url = new URL(request.url!, `http://${request.headers.host}`); - const pathname = url.pathname; + } + private setupUpgradeHandler(httpServer: Server): void { + if (httpServer.listeners('upgrade').length) return; + httpServer.on('upgrade', async (request, socket, head) => { + try { + const url = new URL(request.url!, `http://${request.headers.host}`); + const pathname = url.pathname; - // 从URL查询参数中获取roomId和token - const urlParams = new URLSearchParams(url.search); - const roomId = urlParams.get('roomId'); - const userId = urlParams.get('userId'); - const server = this.servers.find(server => { - const serverPathClean = server.serverPath.replace(/\/$/, ''); - const pathnameClean = pathname.replace(/\/$/, ''); - return serverPathClean === pathnameClean; - }); - - if (!server || !server.wss) { - return socket.destroy(); - } - - server.wss!.handleUpgrade(request, socket, head, (ws: WSClient) => { - ws.userId = userId; - ws.origin = request.url - ws.roomId = roomId - server.wss!.emit('connection', ws, request); - }); - } catch (error) { - this.logger.error('Upgrade error:', error); - socket.destroy(); - } + // 从URL查询参数中获取roomId和token + const urlParams = new URLSearchParams(url.search); + const roomId = urlParams.get('roomId'); + const userId = urlParams.get('userId'); + const server = this.servers.find((server) => { + const serverPathClean = server.serverPath.replace(/\/$/, ''); + const pathnameClean = pathname.replace(/\/$/, ''); + return serverPathClean === pathnameClean; }); - } + + if (!server || !server.wss) { + return socket.destroy(); + } + + server.wss!.handleUpgrade(request, socket, head, (ws: WSClient) => { + ws.userId = userId; + ws.origin = request.url; + ws.roomId = roomId; + server.wss!.emit('connection', ws, request); + }); + } catch (error) { + this.logger.error('Upgrade error:', error); + socket.destroy(); + } + }); + } } diff --git a/apps/server/src/tasks/init/gendev.service.ts b/apps/server/src/tasks/init/gendev.service.ts index 71867c3..76c3542 100755 --- a/apps/server/src/tasks/init/gendev.service.ts +++ b/apps/server/src/tasks/init/gendev.service.ts @@ -38,7 +38,7 @@ export class GenDevService { private readonly departmentService: DepartmentService, private readonly staffService: StaffService, private readonly termService: TermService, - ) { } + ) {} async genDataEvent() { EventBus.emit('genDataEvent', { type: 'start' }); try { @@ -87,16 +87,57 @@ export class GenDevService { // 定义网系类别 const systemTypes = [ - { name: '文印系统', children: ['电源故障', '主板故障', '内存故障', '硬盘故障', '显示器故障', '键盘故障', '鼠标故障'] }, - { name: '内网系统', children: ['系统崩溃', '应用程序错误', '病毒感染', '驱动问题', '系统更新失败'] }, - { name: 'Windows系统', children: ['系统响应慢', '资源占用过高', '过热', '电池寿命短', '存储空间不足'] }, - { name: 'Linux系统', children: ['未知错误', '用户操作错误', '环境因素', '设备老化'] }, - { name: '移动设备系统', children: ['参数设置错误', '配置文件损坏', '兼容性问题', '初始化失败'] }, + { + name: '文印系统', + children: [ + '电源故障', + '主板故障', + '内存故障', + '硬盘故障', + '显示器故障', + '键盘故障', + '鼠标故障', + ], + }, + { + name: '内网系统', + children: [ + '系统崩溃', + '应用程序错误', + '病毒感染', + '驱动问题', + '系统更新失败', + ], + }, + { + name: 'Windows系统', + children: [ + '系统响应慢', + '资源占用过高', + '过热', + '电池寿命短', + '存储空间不足', + ], + }, + { + name: 'Linux系统', + children: ['未知错误', '用户操作错误', '环境因素', '设备老化'], + }, + { + name: '移动设备系统', + children: ['参数设置错误', '配置文件损坏', '兼容性问题', '初始化失败'], + }, ]; // 定义安防设备的子类型 const securityDevices = { - 未知错误: ['未授权访问', '数据泄露', '密码重置', '权限异常', '安全策略冲突'] , + 未知错误: [ + '未授权访问', + '数据泄露', + '密码重置', + '权限异常', + '安全策略冲突', + ], }; // 创建网系类别及其关联的设备类型 diff --git a/apps/server/src/tasks/reminder/reminder.module.ts b/apps/server/src/tasks/reminder/reminder.module.ts index e9e7d91..aad024b 100755 --- a/apps/server/src/tasks/reminder/reminder.module.ts +++ b/apps/server/src/tasks/reminder/reminder.module.ts @@ -4,6 +4,6 @@ import { ReminderService } from './reminder.service'; @Module({ imports: [], providers: [ReminderService], - exports: [ReminderService] + exports: [ReminderService], }) -export class ReminderModule { } +export class ReminderModule {} diff --git a/apps/server/src/tasks/reminder/reminder.service.ts b/apps/server/src/tasks/reminder/reminder.service.ts index e8e939d..65663ef 100755 --- a/apps/server/src/tasks/reminder/reminder.service.ts +++ b/apps/server/src/tasks/reminder/reminder.service.ts @@ -8,73 +8,70 @@ import { Injectable, Logger } from '@nestjs/common'; import dayjs from 'dayjs'; - /** * 提醒服务类 */ @Injectable() export class ReminderService { - /** - * 日志记录器实例 - * @private - */ - private readonly logger = new Logger(ReminderService.name); + /** + * 日志记录器实例 + * @private + */ + private readonly logger = new Logger(ReminderService.name); - /** - * 构造函数 - * @param messageService 消息服务实例 - */ - constructor() { } + /** + * 构造函数 + * @param messageService 消息服务实例 + */ + constructor() {} - /** - * 生成提醒时间点 - * @param totalDays 总天数 - * @returns 提醒时间点数组 - */ - generateReminderTimes(totalDays: number): number[] { - // 如果总天数小于3天则不需要提醒 - if (totalDays < 3) return []; - // 使用Set存储提醒时间点,避免重复 - const reminders: Set = new Set(); - // 按照2的幂次方划分时间点 - for (let i = 1; i <= totalDays / 2; i++) { - reminders.add(Math.ceil(totalDays / Math.pow(2, i))); - } - // 将Set转为数组并升序排序 - return Array.from(reminders).sort((a, b) => a - b); + /** + * 生成提醒时间点 + * @param totalDays 总天数 + * @returns 提醒时间点数组 + */ + generateReminderTimes(totalDays: number): number[] { + // 如果总天数小于3天则不需要提醒 + if (totalDays < 3) return []; + // 使用Set存储提醒时间点,避免重复 + const reminders: Set = new Set(); + // 按照2的幂次方划分时间点 + for (let i = 1; i <= totalDays / 2; i++) { + reminders.add(Math.ceil(totalDays / Math.pow(2, i))); } + // 将Set转为数组并升序排序 + return Array.from(reminders).sort((a, b) => a - b); + } - /** - * 判断是否需要发送提醒 - * @param createdAt 创建时间 - * @param deadline 截止时间 - * @returns 是否需要提醒及剩余天数 - */ - shouldSendReminder(createdAt: Date, deadline: Date) { - // 获取当前时间 - const now = dayjs(); - const end = dayjs(deadline); - // 计算总时间和剩余时间(天) - const totalTimeDays = end.diff(createdAt, 'day'); - const timeLeftDays = end.diff(now, 'day'); + /** + * 判断是否需要发送提醒 + * @param createdAt 创建时间 + * @param deadline 截止时间 + * @returns 是否需要提醒及剩余天数 + */ + shouldSendReminder(createdAt: Date, deadline: Date) { + // 获取当前时间 + const now = dayjs(); + const end = dayjs(deadline); + // 计算总时间和剩余时间(天) + const totalTimeDays = end.diff(createdAt, 'day'); + const timeLeftDays = end.diff(now, 'day'); - if (totalTimeDays > 1) { - // 获取提醒时间点 - const reminderTimes = this.generateReminderTimes(totalTimeDays); - // 如果剩余时间在提醒时间点内,则需要提醒 - if (reminderTimes.includes(timeLeftDays)) { - return { shouldSend: true, timeLeft: timeLeftDays }; - } - } - return { shouldSend: false, timeLeft: timeLeftDays }; + if (totalTimeDays > 1) { + // 获取提醒时间点 + const reminderTimes = this.generateReminderTimes(totalTimeDays); + // 如果剩余时间在提醒时间点内,则需要提醒 + if (reminderTimes.includes(timeLeftDays)) { + return { shouldSend: true, timeLeft: timeLeftDays }; + } } + return { shouldSend: false, timeLeft: timeLeftDays }; + } - /** - * 发送截止日期提醒 - */ - async remindDeadline() { - this.logger.log('开始检查截止日期以发送提醒。'); - - - } + /** + * 发送截止日期提醒 + */ + async remindDeadline() { + this.logger.log('开始检查截止日期以发送提醒。'); + } } diff --git a/apps/server/src/tasks/tasks.module.ts b/apps/server/src/tasks/tasks.module.ts index 64e430f..da49ed3 100755 --- a/apps/server/src/tasks/tasks.module.ts +++ b/apps/server/src/tasks/tasks.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { TasksService } from './tasks.service'; 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({ imports: [InitModule, ReminderModule], - providers: [TasksService] + providers: [TasksService], }) -export class TasksModule { } +export class TasksModule {} diff --git a/apps/server/src/tasks/tasks.service.ts b/apps/server/src/tasks/tasks.service.ts index 8160edf..7e4a817 100755 --- a/apps/server/src/tasks/tasks.service.ts +++ b/apps/server/src/tasks/tasks.service.ts @@ -6,41 +6,47 @@ import { CronJob } from 'cron'; @Injectable() export class TasksService implements OnModuleInit { - private readonly logger = new Logger(TasksService.name); + private readonly logger = new Logger(TasksService.name); - constructor( - private readonly schedulerRegistry: SchedulerRegistry, - private readonly initService: InitService, - private readonly reminderService: ReminderService - ) { } + constructor( + private readonly schedulerRegistry: SchedulerRegistry, + private readonly initService: InitService, + private readonly reminderService: ReminderService, + ) {} - async onModuleInit() { - this.logger.log('Main node launch'); - await this.initService.init(); - this.logger.log('Initialization successful'); + async onModuleInit() { + this.logger.log('Main node launch'); + await this.initService.init(); + this.logger.log('Initialization successful'); + try { + const cronExpression = process.env.DEADLINE_CRON; + if (!cronExpression) { + throw new Error('DEADLINE_CRON environment variable is not set'); + } + + const handleRemindJob = new CronJob(cronExpression, async () => { try { - const cronExpression = process.env.DEADLINE_CRON; - if (!cronExpression) { - throw new Error('DEADLINE_CRON environment variable is not set'); - } - - const handleRemindJob = new CronJob(cronExpression, async () => { - try { - await this.reminderService.remindDeadline(); - this.logger.log('Reminder successfully processed'); - } catch (reminderErr) { - this.logger.error('Error occurred while processing reminder', reminderErr); - } - }); - - this.schedulerRegistry.addCronJob('remindDeadline', handleRemindJob as any); - this.logger.log('Start remind cron job'); - handleRemindJob.start(); - } catch (cronJobErr) { - this.logger.error('Failed to initialize cron job', cronJobErr); - // Optionally rethrow the error if you want to halt further execution - // throw cronJobErr; + await this.reminderService.remindDeadline(); + this.logger.log('Reminder successfully processed'); + } catch (reminderErr) { + this.logger.error( + 'Error occurred while processing reminder', + reminderErr, + ); } + }); + + this.schedulerRegistry.addCronJob( + 'remindDeadline', + handleRemindJob as any, + ); + this.logger.log('Start remind cron job'); + handleRemindJob.start(); + } catch (cronJobErr) { + this.logger.error('Failed to initialize cron job', cronJobErr); + // Optionally rethrow the error if you want to halt further execution + // throw cronJobErr; } + } } diff --git a/apps/server/src/upload/upload.controller.ts b/apps/server/src/upload/upload.controller.ts index 4251037..06bb0fe 100755 --- a/apps/server/src/upload/upload.controller.ts +++ b/apps/server/src/upload/upload.controller.ts @@ -50,7 +50,7 @@ export class UploadController { async handlePost(@Req() req: Request, @Res() res: Response) { return this.tusService.handleTus(req, res); } - + @Get('/*') async handleGet(@Req() req: Request, @Res() res: Response) { return this.tusService.handleTus(req, res); @@ -66,5 +66,4 @@ export class UploadController { async handleUpload(@Req() req: Request, @Res() res: Response) { return this.tusService.handleTus(req, res); } - } diff --git a/apps/server/src/utils/minio/minio.module.ts b/apps/server/src/utils/minio/minio.module.ts index bce8484..363fd7f 100755 --- a/apps/server/src/utils/minio/minio.module.ts +++ b/apps/server/src/utils/minio/minio.module.ts @@ -3,6 +3,6 @@ import { MinioService } from './minio.service'; @Module({ providers: [MinioService], - exports: [MinioService] + exports: [MinioService], }) export class MinioModule {} diff --git a/apps/server/src/utils/redis/utils.ts b/apps/server/src/utils/redis/utils.ts index 341f217..b7bfb0a 100755 --- a/apps/server/src/utils/redis/utils.ts +++ b/apps/server/src/utils/redis/utils.ts @@ -1,13 +1,13 @@ -import { redis } from "./redis.service"; +import { redis } from './redis.service'; export async function deleteByPattern(pattern: string) { - try { - const keys = await redis.keys(pattern); - if (keys.length > 0) { - await redis.del(keys); - // this.logger.log(`Deleted ${keys.length} keys matching pattern ${pattern}`); - } - } catch (error) { - console.error(`Failed to delete keys by pattern ${pattern}:`, error); + try { + const keys = await redis.keys(pattern); + if (keys.length > 0) { + await redis.del(keys); + // this.logger.log(`Deleted ${keys.length} keys matching pattern ${pattern}`); } -} \ No newline at end of file + } catch (error) { + console.error(`Failed to delete keys by pattern ${pattern}:`, error); + } +} diff --git a/apps/server/src/utils/tool.ts b/apps/server/src/utils/tool.ts index 20b6554..adc8d45 100755 --- a/apps/server/src/utils/tool.ts +++ b/apps/server/src/utils/tool.ts @@ -1,148 +1,149 @@ -import { createReadStream } from "fs"; -import { createInterface } from "readline"; +import { createReadStream } from 'fs'; +import { createInterface } from 'readline'; -import { db } from '@nice/common'; -import * as tus from "tus-js-client"; +import { db } from '@nice/common'; +import * as tus from 'tus-js-client'; import ExcelJS from 'exceljs'; export function truncateStringByByte(str, maxBytes) { - let byteCount = 0; - let index = 0; - while (index < str.length && byteCount + new TextEncoder().encode(str[index]).length <= maxBytes) { - byteCount += new TextEncoder().encode(str[index]).length; - index++; - } - return str.substring(0, index) + (index < str.length ? "..." : ""); + let byteCount = 0; + let index = 0; + while ( + index < str.length && + byteCount + new TextEncoder().encode(str[index]).length <= maxBytes + ) { + byteCount += new TextEncoder().encode(str[index]).length; + index++; + } + return str.substring(0, index) + (index < str.length ? '...' : ''); } export async function loadPoliciesFromCSV(filePath: string) { - const policies = { - p: [], - g: [] - }; - const stream = createReadStream(filePath); - const rl = createInterface({ - input: stream, - crlfDelay: Infinity - }); + const policies = { + p: [], + g: [], + }; + const stream = createReadStream(filePath); + const rl = createInterface({ + input: stream, + crlfDelay: Infinity, + }); - // Updated regex to handle commas inside parentheses as part of a single field - const regex = /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g; + // Updated regex to handle commas inside parentheses as part of a single field + const regex = + /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g; - for await (const line of rl) { - // Ignore empty lines and comments - if (line.trim() && !line.startsWith("#")) { - const parts = []; - let match; - while ((match = regex.exec(line)) !== null) { - // Remove quotes if present and trim whitespace - parts.push(match[0].replace(/^"|"$/g, '').trim()); - } + for await (const line of rl) { + // Ignore empty lines and comments + if (line.trim() && !line.startsWith('#')) { + const parts = []; + let match; + while ((match = regex.exec(line)) !== null) { + // Remove quotes if present and trim whitespace + parts.push(match[0].replace(/^"|"$/g, '').trim()); + } - // Check policy type (p or g) - const ptype = parts[0]; - const rule = parts.slice(1); + // Check policy type (p or g) + const ptype = parts[0]; + const rule = parts.slice(1); - if (ptype === 'p' || ptype === 'g') { - policies[ptype].push(rule); - } else { - console.warn(`Unknown policy type '${ptype}' in policy: ${line}`); - } - } + if (ptype === 'p' || ptype === 'g') { + policies[ptype].push(rule); + } else { + console.warn(`Unknown policy type '${ptype}' in policy: ${line}`); + } } + } - return policies; + return policies; } export function uploadFile(blob: any, fileName: string) { - return new Promise((resolve, reject) => { - const upload = new tus.Upload(blob, { - endpoint: `${process.env.TUS_URL}/files/`, - retryDelays: [0, 1000, 3000, 5000], - metadata: { - filename: fileName, - filetype: - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - }, - onError: (error) => { - console.error("Failed because: " + error); - reject(error); // 错误时,我们要拒绝 promise - }, - onProgress: (bytesUploaded, bytesTotal) => { - const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); - // console.log(bytesUploaded, bytesTotal, `${percentage}%`); - }, - onSuccess: () => { - // console.log('Upload finished:', upload.url); - resolve(upload.url); // 成功后,我们解析 promise,并返回上传的 URL - }, - }); - upload.start(); + return new Promise((resolve, reject) => { + const upload = new tus.Upload(blob, { + endpoint: `${process.env.TUS_URL}/files/`, + retryDelays: [0, 1000, 3000, 5000], + metadata: { + filename: fileName, + filetype: + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }, + onError: (error) => { + console.error('Failed because: ' + error); + reject(error); // 错误时,我们要拒绝 promise + }, + onProgress: (bytesUploaded, bytesTotal) => { + const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); + // console.log(bytesUploaded, bytesTotal, `${percentage}%`); + }, + onSuccess: () => { + // console.log('Upload finished:', upload.url); + resolve(upload.url); // 成功后,我们解析 promise,并返回上传的 URL + }, }); + upload.start(); + }); } - class TreeNode { - value: string; - children: TreeNode[]; - - constructor(value: string) { - this.value = value; - this.children = []; - } - - addChild(childValue: string): TreeNode { - let newChild = undefined - if (this.children.findIndex(child => child.value === childValue) === -1) { - newChild = new TreeNode(childValue); - this.children.push(newChild) - - } - return this.children.find(child => child.value === childValue) + value: string; + children: TreeNode[]; + constructor(value: string) { + this.value = value; + this.children = []; + } + + addChild(childValue: string): TreeNode { + let newChild = undefined; + if (this.children.findIndex((child) => child.value === childValue) === -1) { + newChild = new TreeNode(childValue); + this.children.push(newChild); } + return this.children.find((child) => child.value === childValue); + } } function buildTree(data: string[][]): TreeNode { - const root = new TreeNode('root'); - try { - for (const path of data) { - let currentNode = root; - for (const value of path) { - currentNode = currentNode.addChild(value); - } - } - return root; + const root = new TreeNode('root'); + try { + for (const path of data) { + let currentNode = root; + for (const value of path) { + currentNode = currentNode.addChild(value); + } } - catch (error) { - console.error(error) - } - - + return root; + } catch (error) { + console.error(error); + } } export function printTree(node: TreeNode, level: number = 0): void { - const indent = ' '.repeat(level); - // console.log(`${indent}${node.value}`); - for (const child of node.children) { - printTree(child, level + 1); - } + const indent = ' '.repeat(level); + // console.log(`${indent}${node.value}`); + for (const child of node.children) { + printTree(child, level + 1); + } } export async function generateTreeFromFile(file: Buffer): Promise { - const workbook = new ExcelJS.Workbook(); - await workbook.xlsx.load(file); - const worksheet = workbook.getWorksheet(1); + const workbook = new ExcelJS.Workbook(); + await workbook.xlsx.load(file); + const worksheet = workbook.getWorksheet(1); - const data: string[][] = []; + const data: string[][] = []; - worksheet.eachRow((row, rowNumber) => { - if (rowNumber > 1) { // Skip header row if any - const rowData: string[] = (row.values as string[]).slice(2).map(cell => (cell || '').toString()); - data.push(rowData.map(value => value.trim())); - } - }); - // Fill forward values - for (let i = 1; i < data.length; i++) { - for (let j = 0; j < data[i].length; j++) { - if (!data[i][j]) data[i][j] = data[i - 1][j]; - } + worksheet.eachRow((row, rowNumber) => { + if (rowNumber > 1) { + // Skip header row if any + const rowData: string[] = (row.values as string[]) + .slice(2) + .map((cell) => (cell || '').toString()); + data.push(rowData.map((value) => value.trim())); } - return buildTree(data); -} \ No newline at end of file + }); + // Fill forward values + for (let i = 1; i < data.length; i++) { + for (let j = 0; j < data[i].length; j++) { + if (!data[i][j]) data[i][j] = data[i - 1][j]; + } + } + return buildTree(data); +} diff --git a/apps/server/src/validation/index.ts b/apps/server/src/validation/index.ts index 8f49df8..58a8c26 100755 --- a/apps/server/src/validation/index.ts +++ b/apps/server/src/validation/index.ts @@ -2,17 +2,17 @@ import { PipeTransform, BadRequestException } from '@nestjs/common'; import { ZodSchema } from 'zod'; export class ZodValidationPipe implements PipeTransform { - constructor(private schema: ZodSchema) { } + constructor(private schema: ZodSchema) {} - transform(value: unknown) { - try { - const result = this.schema.parse(value); - return result; - } catch (error: any) { - throw new BadRequestException('Validation failed', { - cause: error, - description: error.errors - }); - } + transform(value: unknown) { + try { + const result = this.schema.parse(value); + return result; + } catch (error: any) { + throw new BadRequestException('Validation failed', { + cause: error, + description: error.errors, + }); } -} \ No newline at end of file + } +} diff --git a/apps/web/index.html b/apps/web/index.html old mode 100755 new mode 100644 index 4af329b..9ac1594 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -14,7 +14,7 @@ VITE_APP_FILE_PORT: "$FILE_PORT", }; - $APP_NAME + 故障检索 diff --git a/apps/web/src/app/main/devicepage/dashboard/page.tsx b/apps/web/src/app/main/devicepage/dashboard/page.tsx index 37989f2..5214296 100644 --- a/apps/web/src/app/main/devicepage/dashboard/page.tsx +++ b/apps/web/src/app/main/devicepage/dashboard/page.tsx @@ -1,10 +1,390 @@ -const DashboardPage = () => { +import { useState, useEffect, useRef } from 'react'; +import { Card, Row, Col, DatePicker, Select, Spin } from 'antd'; +import ReactECharts from 'echarts-for-react'; +import dayjs from 'dayjs'; +import { api } from "@nice/client"; - return ( -
-

Dashboard

-
- ); +const { RangePicker } = DatePicker; +const { Option } = Select; + +const DashboardPage = () => { + const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>([ + dayjs().subtract(30, 'days'), + dayjs() + ]); + const [loading, setLoading] = useState(true); + + + // 使用useRef跟踪是否已执行过刷新 + const hasRefetched = useRef(false); + + // 获取网系类别数据 + const { data: systemTypeTerms, isLoading: loadingTypes, refetch: refetchSypes } = api.term.findMany.useQuery({ + where: { + taxonomy: { slug: "system_type" }, + deletedAt: null, + }, + orderBy: { order: "asc" }, + }); + + // 获取设备故障数据 + const { data: devices, isLoading: loadingDevices, refetch: refetchDevices } = api.device.findMany.useQuery({ + where: { + deletedAt: null, + createdAt: { + // 将开始日期设置为当天的开始时间 00:00:00 + gte: dateRange[0].startOf('day').toISOString(), + // 将结束日期设置为当天的结束时间 23:59:59 + lte: dateRange[1].endOf('day').toISOString(), + } + }, + include: { + department: true, + }, + }); + useEffect(() => { + // 只在数据加载完成后执行一次刷新 + if (!loadingTypes && !loadingDevices && !hasRefetched.current) { + // 标记为已执行 + hasRefetched.current = true + // 刷新数据 + refetchSypes(); + refetchDevices(); + setLoading(false); + } + }, [loadingTypes, loadingDevices]); + + + // 处理日期范围变化 + const handleDateRangeChange = (dates, dateStrings) => { + if (dates) { + setDateRange([dates[0], dates[1]]); + } else { + setDateRange([dayjs().subtract(30, 'days'), dayjs()]); + } + }; + + // 准备各个网系故障情况数据(按时间段) + const prepareSystemFaultsByTimeData = () => { + if (!devices || !systemTypeTerms) return { xAxis: [], series: [] }; + + // 获取选择的日期范围 + const startDate = dateRange[0]; + const endDate = dateRange[1]; + const diffDays = endDate.diff(startDate, 'day'); + + // 根据时间跨度选择合适的间隔 + const intervals = []; + let format = ''; + + if (diffDays <= 14) { + // 小于等于两周,按天展示 + format = 'MM-DD'; + for (let i = 0; i <= diffDays; i++) { + intervals.push(startDate.add(i, 'day')); + } + } else if (diffDays <= 90) { + // 小于等于3个月,按周展示 + format = 'MM-DD'; + const weeks = Math.ceil(diffDays / 7); + for (let i = 0; i < weeks; i++) { + const weekStart = startDate.add(i * 7, 'day'); + intervals.push(weekStart); + } + } else { + // 大于3个月,按月展示 + format = 'YYYY-MM'; + const months = Math.ceil(diffDays / 30); + for (let i = 0; i < months; i++) { + const monthStart = startDate.add(i, 'month').startOf('month'); + intervals.push(monthStart); + } + } + + // 格式化时间段显示 + const timeLabels = intervals.map(date => { + if (diffDays <= 14) { + return date.format(format); + } else if (diffDays <= 90) { + return `${date.format(format)}至${date.add(6, 'day').format(format)}`; + } else { + return date.format(format); + } + }); + + // 统计每个网系在每个时间段的故障数 + const systemNames = systemTypeTerms.map(type => type.name); + const systemIdMap = {}; + systemTypeTerms.forEach(type => { + systemIdMap[type.id] = type.name; + }); + + const seriesData = systemNames.map(name => ({ + name, + type: 'bar', + data: new Array(intervals.length).fill(0) + })); + + devices.forEach(device => { + const systemName = systemIdMap[device.systemType] || '未知'; + const systemIndex = systemNames.indexOf(systemName); + if (systemIndex !== -1) { + const createDate = dayjs(device.createdAt); + + for (let i = 0; i < intervals.length; i++) { + const nextInterval = null; + + if (diffDays <= 14) { + // 按天,检查是否是同一天 + if (createDate.format('YYYY-MM-DD') === intervals[i].format('YYYY-MM-DD')) { + seriesData[systemIndex].data[i]++; + } + } else if (diffDays <= 90) { + // 按周,检查是否在这周内 + const weekEnd = intervals[i].add(6, 'day').endOf('day'); + if (createDate.isAfter(intervals[i]) && createDate.isBefore(weekEnd)) { + seriesData[systemIndex].data[i]++; + } + } else { + // 按月,检查是否在这个月内 + const monthEnd = intervals[i].endOf('month'); + if (createDate.isAfter(intervals[i]) && createDate.isBefore(monthEnd)) { + seriesData[systemIndex].data[i]++; + } + } + } + } + }); + + return { + xAxis: timeLabels, + series: seriesData + }; + }; + + // 准备各个网系故障率数据 + const prepareSystemFaultRateData = () => { + if (!devices || !systemTypeTerms) return { names: [], values: [] }; + + const systemCounts = {}; + const totalDevices = devices.length; + console.log("devices", devices.length); + + // 初始化所有网系的计数 + systemTypeTerms.forEach(type => { + systemCounts[type.name] = 0; + }); + + // 统计每个网系的故障数 + devices.forEach(device => { + const systemType = systemTypeTerms?.find(t => t.id === device.systemType); + if (systemType) { + systemCounts[systemType.name] = (systemCounts[systemType.name] || 0) + 1; + } + }); + + const names = Object.keys(systemCounts); + const values = names.map(name => ({ + value: totalDevices ? ((systemCounts[name] / totalDevices) * 100).toFixed(2) : 0, + name + })); + + return { names, values }; + }; + + // 准备故障处置完成率数据 + const prepareFaultCompletionRateData = () => { + if (!devices) return { value: 0 }; + const completedFaults = devices.filter(device => + device.deviceStatus === 'normal' + ).length; + + const totalFaults = devices.length; + const completionRate = totalFaults ? (completedFaults / totalFaults) * 100 : 0; + + return { value: parseFloat(completionRate.toFixed(2)) }; + }; + + // 网系故障时间段分布选项 + const getSystemFaultsByTimeOption = () => { + const data = prepareSystemFaultsByTimeData(); + return { + title: { + text: '各网系故障时间分布', + left: 'center' + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + legend: { + data: data.series.map(item => item.name), + bottom: 10 + }, + grid: { + left: '3%', + right: '4%', + bottom: '15%', + top: '15%', + containLabel: true + }, + xAxis: { + type: 'category', + data: data.xAxis + }, + yAxis: { + type: 'value', + name: '故障数量' + }, + series: data.series + }; + }; + + // 网系故障率选项 + const getSystemFaultRateOption = () => { + const data = prepareSystemFaultRateData(); + return { + title: { + text: '各网系故障率', + left: 'center' + }, + tooltip: { + trigger: 'item', + formatter: '{a}
{b}: {c}%' + }, + legend: { + orient: 'vertical', + left: 10, + top: 'center', + data: data.names + }, + series: [ + { + name: '故障率', + type: 'pie', + radius: ['40%', '70%'], + avoidLabelOverlap: false, + itemStyle: { + borderRadius: 10, + borderColor: '#fff', + borderWidth: 2 + }, + label: { + show: true, + formatter: '{b}: {c}%' + }, + emphasis: { + label: { + show: true, + fontSize: '15', + fontWeight: 'bold' + } + }, + data: data.values + } + ] + }; + }; + + // 故障处置完成率选项 + const getFaultCompletionRateOption = () => { + const data = prepareFaultCompletionRateData(); + return { + title: { + text: '故障处置完成率', + left: 'center' + }, + tooltip: { + formatter: '{a}
{b} : {c}%' + }, + series: [ + { + name: '完成率', + type: 'gauge', + detail: { formatter: '{value}%' }, + data: [{ value: data.value, name: '完成率' }], + axisLine: { + lineStyle: { + width: 30, + color: [ + [0.3, '#ff6e76'], + [0.7, '#fddd60'], + [1, '#7cffb2'] + ] + } + }, + pointer: { + itemStyle: { + color: 'auto' + } + } + } + ] + }; + }; + + return ( +
+

故障数据可视化面板

+ {/* */} + + + + +
+ 数据筛选 + +
+
+ +
+ + {loading ? ( +
+ +
+ ) : ( + + + + + + + + + + + + + + + + + + + )} +
+ ); }; -export default DashboardPage; +export default DashboardPage; \ No newline at end of file diff --git a/apps/web/src/app/main/devicepage/devicemodal/page.tsx b/apps/web/src/app/main/devicepage/devicemodal/page.tsx index 6b964f4..60a4b50 100755 --- a/apps/web/src/app/main/devicepage/devicemodal/page.tsx +++ b/apps/web/src/app/main/devicepage/devicemodal/page.tsx @@ -85,7 +85,7 @@ export default function DeviceModal() { label="故障名称 " rules={[{ required: true, message: "请输入故障名称" }]} > - + @@ -98,11 +98,10 @@ export default function DeviceModal() { - + 已修复 维修中 - 损坏 - 闲置 + 未修复 diff --git a/apps/web/src/app/main/devicepage/devicetable/page.tsx b/apps/web/src/app/main/devicepage/devicetable/page.tsx index 20c5ef3..3873f94 100755 --- a/apps/web/src/app/main/devicepage/devicetable/page.tsx +++ b/apps/web/src/app/main/devicepage/devicetable/page.tsx @@ -1,4 +1,4 @@ -import { Button, Checkbox, Modal, Table, Upload } from "antd"; +import { Button, Checkbox, Modal, Table, Upload, Tag } from "antd"; // 添加 Tag 导入 import { ColumnsType } from "antd/es/table"; import { api, useDevice, useStaff } from "@nice/client"; import { useEffect, useState, useImperativeHandle, forwardRef, useRef } from "react"; @@ -51,6 +51,8 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => enabled: true, } ); + console.log("devices",devices); + const { data: systemTypeTerms, refetch: refetchSystemType } = api.term.findMany.useQuery({ where: { @@ -181,21 +183,39 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => key: "deviceStatus", align: "center", render: (status) => { - const statusMap = { - normal: "正常", - maintenance: "维修中", - broken: "损坏", - idle: "闲置" + const statusConfig = { + normal: { + text: "已修复", + color: "success" + }, + maintenance: { + text: "维修中", + color: "processing" + }, + broken: { + text: "未修复", + color: "error" + } }; - return statusMap[status] || "未知"; - }, + + const config = statusConfig[status] || { + text: "未知", + color: "default" + }; + + return ( + + {config.text} + + ); + } }, { title: "时间", dataIndex: "createdAt", key: "createdAt", align: "center", - render: (text, record) => record.createdAt ? dayjs(record.createdAt).format('YYYY-MM-DD') : "未知", + render: (text, record) => record.createdAt ? dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss') : "未知", }, { title: "操作", @@ -262,7 +282,7 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => '故障名称': item?.showname || "未命名故障", '故障状态': (() => { const statusMap = { - normal: "正常", + normal: "已修复", maintenance: "维修中", broken: "损坏", idle: "闲置" @@ -322,7 +342,7 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => // 确认是否有有效数据 if (records.length === 0) { toast.error("未找到有效数据"); - return; + return; } // 批量创建记录 await batchImportRecords(records); @@ -366,10 +386,9 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => // 获取状态键 const getStatusKeyByValue = (value: string) => { const statusMap: Record = { - "正常": "normal", + "已修复": "normal", "维修中": "maintenance", - "损坏": "broken", - "闲置": "idle" + "未修复 ": "broken", }; return statusMap[value] || "normal"; }; @@ -388,15 +407,12 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => toast.error("没有找到有效的记录数据"); return; } - // 设置批处理大小 const batchSize = 5; let successCount = 0; let totalProcessed = 0; - // 显示进度提示 const loadingToast = toast.loading(`正在导入数据...`); - // 分批处理数据 for (let i = 0; i < validRecords.length; i += batchSize) { const batch = validRecords.slice(i, i + batchSize); @@ -444,7 +460,7 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => '故障类型': deviceTypeTerms?.[0]?.name || '故障类型1', '单位': '单位名称', '故障名称': '示例故障名称', - '故障状态': '正常', // 可选值: 正常、维修中、损坏、闲置 + '故障状态': '未修复', // 可选值:已修复, 维修中, 未修复 '描述': '这是一个示例描述' } ]; @@ -479,7 +495,7 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) =>
0 && selectedRowKeys?.length < (devices?.length || 0)} - checked={(devices?.length || 0) > 0 && selectedRowKeys?.length === (devices?.length || 0)} + checked={(devices?.length || 0) > 0 && selectedRowKeys?.length === (devices?.length || 0)} onChange={(e) => { const checked = e.target.checked; const newSelectedRowKeys = checked ? (devices || []).map(item => item.id) : []; @@ -490,7 +506,10 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) =>
), preserveSelectedRowKeys: true // 这个属性保证翻页时选中状态不丢失 + } + + const TableHeader = () => (
@@ -562,6 +581,10 @@ const DeviceTable = forwardRef(({ onSelectedChange }: DeviceTableProps, ref) => pageSize: 10, showSizeChanger: true, pageSizeOptions: ["10", "20", "30"], + hideOnSinglePage: true, + responsive: true, + showTotal: (total, range) => `共${total} 条数据`, + showQuickJumper: true, }} components={{ header: { diff --git a/apps/web/src/app/main/devicepage/page.tsx b/apps/web/src/app/main/devicepage/page.tsx index aa13e96..8b081b8 100755 --- a/apps/web/src/app/main/devicepage/page.tsx +++ b/apps/web/src/app/main/devicepage/page.tsx @@ -20,6 +20,7 @@ import DepartmentSelect from "@web/src/components/models/department/department-s import SystemTypeSelect from "@web/src/app/main/devicepage/select/System-select"; import DeviceTypeSelect from "@web/src/app/main/devicepage/select/Device-select"; import dayjs from "dayjs"; +import FixTypeSelect from "./select/Fix-select"; // 添加筛选条件类型 type SearchCondition = { deletedAt: null; @@ -58,6 +59,7 @@ export default function DeviceMessage() { const [location, setLocation] = useState(""); const [status, setStatus] = useState(null); const [selectedSystemTypeId, setSelectedSystemTypeId] = useState(""); + const [selectedFixType, setSelectedFixType] = useState(null); // 创建ref以访问DeviceTable内部方法 const tableRef = useRef(null); @@ -78,6 +80,7 @@ export default function DeviceMessage() { deletedAt: null, ...(selectedSystem && { systemType: selectedSystem }), ...(selectedDeviceType && { deviceType: selectedDeviceType }), + ...(selectedFixType && { deviceStatus: selectedFixType }), ...(selectedDept && { deptId: selectedDept }), ...(time && { createdAt: { @@ -120,7 +123,7 @@ export default function DeviceMessage() { }; // 处理选择变更的回调 - const handleSelectedChange = (keys: React.Key[], data: any[]) => { + const handleSelectedChange = (keys: React.Key[], data: any[]) => { console.log("选中状态变化:", keys.length); setSelectedKeys(keys); setSelectedData(data); @@ -144,7 +147,7 @@ export default function DeviceMessage() {

故障收录检索

@@ -165,6 +168,14 @@ export default function DeviceMessage() { className="w-full" systemTypeId={selectedSystemTypeId} /> +
+
+
void; + placeholder?: string; + disabled?: boolean; + className?: string; + style?: React.CSSProperties; +} + +export default function FixTypeSelect({ value,onChange,placeholder = "选择故障状态",disabled = false,className,style,}: FixTypeSelectProps) { + // 故障状态是固定的,直接定义选项 + const options = [ + { label: "已修复", value: "normal" }, + { label: "维修中", value: "maintenance" }, + { label: "未修复", value: "broken" }, + ]; + + return ( +