This commit is contained in:
Your Name 2025-05-27 16:48:09 +08:00
parent 9075be6046
commit 678ad87c2f
97 changed files with 3008 additions and 2623 deletions

View File

@ -23,12 +23,12 @@ import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, // 全局可用 isGlobal: true, // 全局可用
envFilePath: '.env' envFilePath: '.env',
}), }),
ScheduleModule.forRoot(), ScheduleModule.forRoot(),
JwtModule.register({ JwtModule.register({
global: true, global: true,
secret: env.JWT_SECRET secret: env.JWT_SECRET,
}), }),
WebSocketModule, WebSocketModule,
TrpcModule, TrpcModule,
@ -43,11 +43,13 @@ import { SystemLogModule } from '@server/models/sys-logs/systemLog.module';
CollaborationModule, CollaborationModule,
RealTimeModule, RealTimeModule,
UploadModule, UploadModule,
SystemLogModule SystemLogModule,
],
providers: [
{
provide: APP_FILTER,
useClass: ExceptionsFilter,
},
], ],
providers: [{
provide: APP_FILTER,
useClass: ExceptionsFilter,
}],
}) })
export class AppModule { } export class AppModule {}

View File

@ -43,8 +43,9 @@ export class AuthController {
authorization, authorization,
}; };
const authResult = const authResult = await this.authService.validateFileRequest(
await this.authService.validateFileRequest(fileRequest); fileRequest,
);
if (!authResult.isValid) { if (!authResult.isValid) {
// 使用枚举类型进行错误处理 // 使用枚举类型进行错误处理
switch (authResult.error) { switch (authResult.error) {

View File

@ -1,8 +1,8 @@
import { import {
CanActivate, CanActivate,
ExecutionContext, ExecutionContext,
Injectable, Injectable,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import { env } from '@server/env'; import { env } from '@server/env';
@ -12,26 +12,21 @@ import { extractTokenFromHeader } from './utils';
@Injectable() @Injectable()
export class AuthGuard implements CanActivate { export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) { } constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const token = extractTokenFromHeader(request); const token = extractTokenFromHeader(request);
if (!token) { if (!token) {
throw new UnauthorizedException(); 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;
} }
try {
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
secret: env.JWT_SECRET,
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
} }

View File

@ -8,12 +8,8 @@ import { SessionService } from './session.service';
import { RoleMapModule } from '@server/models/rbac/rbac.module'; import { RoleMapModule } from '@server/models/rbac/rbac.module';
@Module({ @Module({
imports: [StaffModule, RoleMapModule], imports: [StaffModule, RoleMapModule],
providers: [ providers: [AuthService, TrpcService, DepartmentService, SessionService],
AuthService,
TrpcService,
DepartmentService,
SessionService],
exports: [AuthService], exports: [AuthService],
controllers: [AuthController], controllers: [AuthController],
}) })
export class AuthModule { } export class AuthModule {}

View File

@ -1,9 +1,9 @@
export const tokenConfig = { export const tokenConfig = {
accessToken: { accessToken: {
expirationMs: 7 * 24 * 3600000, // 7 days expirationMs: 7 * 24 * 3600000, // 7 days
expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds
}, },
refreshToken: { refreshToken: {
expirationMs: 30 * 24 * 3600000, // 30 days expirationMs: 30 * 24 * 3600000, // 30 days
}, },
}; };

View File

@ -4,58 +4,63 @@ import { redis } from '@server/utils/redis/redis.service';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
export interface SessionInfo { export interface SessionInfo {
session_id: string; session_id: string;
access_token: string; access_token: string;
access_token_expires_at: number; access_token_expires_at: number;
refresh_token: string; refresh_token: string;
refresh_token_expires_at: number; refresh_token_expires_at: number;
} }
@Injectable() @Injectable()
export class SessionService { export class SessionService {
private getSessionKey(userId: string, sessionId: string): string { private getSessionKey(userId: string, sessionId: string): string {
return `session-${userId}-${sessionId}`; return `session-${userId}-${sessionId}`;
} }
async createSession( async createSession(
userId: string, userId: string,
accessToken: string, accessToken: string,
refreshToken: string, refreshToken: string,
expirationConfig: { expirationConfig: {
accessTokenExpirationMs: number; accessTokenExpirationMs: number;
refreshTokenExpirationMs: number; refreshTokenExpirationMs: number;
sessionTTL: number; sessionTTL: number;
}, },
): Promise<SessionInfo> { ): Promise<SessionInfo> {
const sessionInfo: SessionInfo = { const sessionInfo: SessionInfo = {
session_id: uuidv4(), session_id: uuidv4(),
access_token: accessToken, access_token: accessToken,
access_token_expires_at: Date.now() + expirationConfig.accessTokenExpirationMs, access_token_expires_at:
refresh_token: refreshToken, Date.now() + expirationConfig.accessTokenExpirationMs,
refresh_token_expires_at: Date.now() + expirationConfig.refreshTokenExpirationMs, refresh_token: refreshToken,
}; refresh_token_expires_at:
Date.now() + expirationConfig.refreshTokenExpirationMs,
};
await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL); await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL);
return sessionInfo; return sessionInfo;
} }
async getSession(userId: string, sessionId: string): Promise<SessionInfo | null> { async getSession(
const sessionData = await redis.get(this.getSessionKey(userId, sessionId)); userId: string,
return sessionData ? JSON.parse(sessionData) : null; sessionId: string,
} ): Promise<SessionInfo | null> {
const sessionData = await redis.get(this.getSessionKey(userId, sessionId));
return sessionData ? JSON.parse(sessionData) : null;
}
async saveSession( async saveSession(
userId: string, userId: string,
sessionInfo: SessionInfo, sessionInfo: SessionInfo,
ttl: number, ttl: number,
): Promise<void> { ): Promise<void> {
await redis.setex( await redis.setex(
this.getSessionKey(userId, sessionInfo.session_id), this.getSessionKey(userId, sessionInfo.session_id),
ttl, ttl,
JSON.stringify(sessionInfo), JSON.stringify(sessionInfo),
); );
} }
async deleteSession(userId: string, sessionId: string): Promise<void> { async deleteSession(userId: string, sessionId: string): Promise<void> {
await redis.del(this.getSessionKey(userId, sessionId)); await redis.del(this.getSessionKey(userId, sessionId));
} }
} }

View File

@ -1,31 +1,31 @@
export interface TokenConfig { export interface TokenConfig {
accessToken: { accessToken: {
expirationMs: number; expirationMs: number;
expirationTTL: number; expirationTTL: number;
}; };
refreshToken: { refreshToken: {
expirationMs: number; expirationMs: number;
}; };
} }
export interface FileAuthResult { export interface FileAuthResult {
isValid: boolean isValid: boolean;
userId?: string userId?: string;
resourceType?: string resourceType?: string;
error?: string error?: string;
} }
export interface FileRequest { export interface FileRequest {
originalUri: string; originalUri: string;
realIp: string; realIp: string;
method: string; method: string;
queryParams: string; queryParams: string;
host: string; host: string;
authorization: string authorization: string;
} }
export enum FileValidationErrorType { export enum FileValidationErrorType {
INVALID_URI = 'INVALID_URI', INVALID_URI = 'INVALID_URI',
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED', AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED',
INVALID_TOKEN = 'INVALID_TOKEN', INVALID_TOKEN = 'INVALID_TOKEN',
UNKNOWN_ERROR = 'UNKNOWN_ERROR' UNKNOWN_ERROR = 'UNKNOWN_ERROR',
} }

View File

@ -11,7 +11,7 @@ import { env } from '@server/env';
import { redis } from '@server/utils/redis/redis.service'; import { redis } from '@server/utils/redis/redis.service';
import EventBus from '@server/utils/event-bus'; import EventBus from '@server/utils/event-bus';
import { RoleMapService } from '@server/models/rbac/rolemap.service'; import { RoleMapService } from '@server/models/rbac/rolemap.service';
import { Request } from "express" import { Request } from 'express';
interface ProfileResult { interface ProfileResult {
staff: UserProfile | undefined; staff: UserProfile | undefined;
error?: string; error?: string;
@ -22,9 +22,11 @@ interface TokenVerifyResult {
error?: string; error?: string;
} }
export function extractTokenFromHeader(request: Request): string | undefined { export function extractTokenFromHeader(request: Request): string | undefined {
return extractTokenFromAuthorization(request.headers.authorization) return extractTokenFromAuthorization(request.headers.authorization);
} }
export function extractTokenFromAuthorization(authorization: string): string | undefined { export function extractTokenFromAuthorization(
authorization: string,
): string | undefined {
const [type, token] = authorization?.split(' ') ?? []; const [type, token] = authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined; return type === 'Bearer' ? token : undefined;
} }
@ -40,7 +42,7 @@ export class UserProfileService {
this.jwtService = new JwtService(); this.jwtService = new JwtService();
this.departmentService = new DepartmentService(); this.departmentService = new DepartmentService();
this.roleMapService = new RoleMapService(this.departmentService); this.roleMapService = new RoleMapService(this.departmentService);
EventBus.on("dataChanged", ({ type, data }) => { EventBus.on('dataChanged', ({ type, data }) => {
if (type === ObjectType.STAFF) { if (type === ObjectType.STAFF) {
// 确保 data 是数组,如果不是则转换为数组 // 确保 data 是数组,如果不是则转换为数组
const dataArray = Array.isArray(data) ? data : [data]; const dataArray = Array.isArray(data) ? data : [data];
@ -51,7 +53,6 @@ export class UserProfileService {
} }
} }
}); });
} }
public getProfileCacheKey(id: string) { public getProfileCacheKey(id: string) {
return `user-profile-${id}`; return `user-profile-${id}`;
@ -175,9 +176,7 @@ export class UserProfileService {
staff.deptId staff.deptId
? this.departmentService.getDescendantIdsInDomain(staff.deptId) ? this.departmentService.getDescendantIdsInDomain(staff.deptId)
: [], : [],
staff.deptId staff.deptId ? this.departmentService.getAncestorIds([staff.deptId]) : [],
? this.departmentService.getAncestorIds([staff.deptId])
: [],
this.roleMapService.getPermsForObject({ this.roleMapService.getPermsForObject({
domainId: staff.domainId, domainId: staff.domainId,
staffId: staff.id, staffId: staff.id,

View File

@ -1,3 +1,4 @@
export const env: { JWT_SECRET: string } = { export const env: { JWT_SECRET: string } = {
JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA=' JWT_SECRET:
} process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA=',
};

View File

@ -7,6 +7,6 @@ import { RealTimeModule } from '@server/socket/realtime/realtime.module';
@Module({ @Module({
imports: [RealTimeModule], imports: [RealTimeModule],
providers: [AppConfigService, AppConfigRouter, TrpcService], providers: [AppConfigService, AppConfigRouter, TrpcService],
exports: [AppConfigService, AppConfigRouter] exports: [AppConfigService, AppConfigRouter],
}) })
export class AppConfigModule { } export class AppConfigModule {}

View File

@ -1,198 +1,236 @@
import { import {
BadRequestException, BadRequestException,
NotFoundException, NotFoundException,
ConflictException, ConflictException,
InternalServerErrorException, InternalServerErrorException,
} from '@nestjs/common'; } from '@nestjs/common';
export const PrismaErrorCode = Object.freeze({ export const PrismaErrorCode = Object.freeze({
P2000: 'P2000', P2000: 'P2000',
P2001: 'P2001', P2001: 'P2001',
P2002: 'P2002', P2002: 'P2002',
P2003: 'P2003', P2003: 'P2003',
P2006: 'P2006', P2006: 'P2006',
P2007: 'P2007', P2007: 'P2007',
P2008: 'P2008', P2008: 'P2008',
P2009: 'P2009', P2009: 'P2009',
P2010: 'P2010', P2010: 'P2010',
P2011: 'P2011', P2011: 'P2011',
P2012: 'P2012', P2012: 'P2012',
P2014: 'P2014', P2014: 'P2014',
P2015: 'P2015', P2015: 'P2015',
P2016: 'P2016', P2016: 'P2016',
P2017: 'P2017', P2017: 'P2017',
P2018: 'P2018', P2018: 'P2018',
P2019: 'P2019', P2019: 'P2019',
P2021: 'P2021', P2021: 'P2021',
P2023: 'P2023', P2023: 'P2023',
P2025: 'P2025', P2025: 'P2025',
P2031: 'P2031', P2031: 'P2031',
P2033: 'P2033', P2033: 'P2033',
P2034: 'P2034', P2034: 'P2034',
P2037: 'P2037', P2037: 'P2037',
P1000: 'P1000', P1000: 'P1000',
P1001: 'P1001', P1001: 'P1001',
P1002: 'P1002', P1002: 'P1002',
P1015: 'P1015', P1015: 'P1015',
P1017: 'P1017', P1017: 'P1017',
}); });
export type PrismaErrorCode = keyof typeof PrismaErrorCode; export type PrismaErrorCode = keyof typeof PrismaErrorCode;
interface PrismaErrorMeta {
target?: string;
model?: string;
relationName?: string;
details?: string;
}
interface PrismaErrorMeta { export type operationT = 'create' | 'read' | 'update' | 'delete';
target?: string;
model?: string;
relationName?: string;
details?: string;
}
export type operationT = 'create' | 'read' | 'update' | 'delete'; export type PrismaErrorHandler = (
operation: operationT,
meta?: PrismaErrorMeta,
) => Error;
export type PrismaErrorHandler = ( export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = {
operation: operationT, P2000: (_operation, meta) =>
meta?: PrismaErrorMeta, new BadRequestException(
) => Error; `The provided value for ${
meta?.target || 'a field'
} is too long. Please use a shorter value.`,
),
export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = { P2001: (operation, meta) =>
P2000: (_operation, meta) => new BadRequestException( new NotFoundException(
`The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.` `The ${
), meta?.model || 'record'
} you are trying to ${operation} could not be found.`,
),
P2001: (operation, meta) => new NotFoundException( P2002: (operation, meta) => {
`The ${meta?.model || 'record'} you are trying to ${operation} could not be found.` 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}.`,
);
}
},
P2002: (operation, meta) => { P2003: (operation) =>
const field = meta?.target || 'unique field'; new BadRequestException(
switch (operation) { `Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`,
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}.`
);
}
},
P2003: (operation) => new BadRequestException( P2006: (_operation, meta) =>
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.` new BadRequestException(
), `The provided value for ${
meta?.target || 'a field'
} is invalid. Please correct it.`,
),
P2006: (_operation, meta) => new BadRequestException( P2007: (operation) =>
`The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.` new InternalServerErrorException(
), `Data validation error during ${operation}. Please ensure all inputs are valid and try again.`,
),
P2007: (operation) => new InternalServerErrorException( P2008: (operation) =>
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.` new InternalServerErrorException(
), `Failed to query the database during ${operation}. Please try again later.`,
),
P2008: (operation) => new InternalServerErrorException( P2009: (operation) =>
`Failed to query the database during ${operation}. Please try again later.` new InternalServerErrorException(
), `Invalid data fetched during ${operation}. Check query structure.`,
),
P2009: (operation) => new InternalServerErrorException( P2010: () =>
`Invalid data fetched during ${operation}. Check query structure.` new InternalServerErrorException(
), `Invalid raw query. Ensure your query is correct and try again.`,
),
P2010: () => new InternalServerErrorException( P2011: (_operation, meta) =>
`Invalid raw query. Ensure your query is correct and try again.` new BadRequestException(
), `The required field ${
meta?.target || 'a field'
} is missing. Please provide it to continue.`,
),
P2011: (_operation, meta) => new BadRequestException( P2012: (operation, meta) =>
`The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.` new BadRequestException(
), `Missing required relation ${
meta?.relationName || ''
}. Ensure all related data exists before ${operation}.`,
),
P2012: (operation, meta) => new BadRequestException( P2014: (operation) => {
`Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${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.`);
}
},
P2014: (operation) => { P2015: () =>
switch (operation) { new InternalServerErrorException(
case 'create': `A record with the required ID was expected but not found. Please retry.`,
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.`);
}
},
P2015: () => new InternalServerErrorException( P2016: (operation) =>
`A record with the required ID was expected but not found. Please retry.` new InternalServerErrorException(
), `Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`,
),
P2016: (operation) => new InternalServerErrorException( P2017: (operation) =>
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.` new InternalServerErrorException(
), `Connected records were not found for ${operation}. Check related data.`,
),
P2017: (operation) => new InternalServerErrorException( P2018: () =>
`Connected records were not found for ${operation}. Check related data.` new InternalServerErrorException(
), `The required connection could not be established. Please check relationships.`,
),
P2018: () => new InternalServerErrorException( P2019: (_operation, meta) =>
`The required connection could not be established. Please check relationships.` new InternalServerErrorException(
), `Invalid input for ${
meta?.details || 'a field'
}. Please ensure data conforms to expectations.`,
),
P2019: (_operation, meta) => new InternalServerErrorException( P2021: (_operation, meta) =>
`Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.` new InternalServerErrorException(
), `The ${meta?.model || 'model'} was not found in the database.`,
),
P2021: (_operation, meta) => new InternalServerErrorException( P2025: (operation, meta) =>
`The ${meta?.model || 'model'} was not found in the database.` new NotFoundException(
), `The ${
meta?.model || 'record'
} you are trying to ${operation} does not exist. It may have been deleted.`,
),
P2025: (operation, meta) => new NotFoundException( P2031: () =>
`The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.` new InternalServerErrorException(
), `Invalid Prisma Client initialization error. Please check configuration.`,
),
P2031: () => new InternalServerErrorException( P2033: (operation) =>
`Invalid Prisma Client initialization error. Please check configuration.` new InternalServerErrorException(
), `Insufficient database write permissions for ${operation}.`,
),
P2033: (operation) => new InternalServerErrorException( P2034: (operation) =>
`Insufficient database write permissions for ${operation}.` new InternalServerErrorException(
), `Database read-only transaction failed during ${operation}.`,
),
P2034: (operation) => new InternalServerErrorException( P2037: (operation) =>
`Database read-only transaction failed during ${operation}.` new InternalServerErrorException(
), `Unsupported combinations of input types for ${operation}. Please correct the query or input.`,
),
P2037: (operation) => new InternalServerErrorException( P1000: () =>
`Unsupported combinations of input types for ${operation}. Please correct the query or input.` new InternalServerErrorException(
), `Database authentication failed. Verify your credentials and try again.`,
),
P1000: () => new InternalServerErrorException( P1001: () =>
`Database authentication failed. Verify your credentials and try again.` new InternalServerErrorException(
), `The database server could not be reached. Please check its availability.`,
),
P1001: () => new InternalServerErrorException( P1002: () =>
`The database server could not be reached. Please check its availability.` new InternalServerErrorException(
), `Connection to the database timed out. Verify network connectivity and server availability.`,
),
P1002: () => new InternalServerErrorException( P1015: (operation) =>
`Connection to the database timed out. Verify network connectivity and server availability.` new InternalServerErrorException(
), `Migration failed. Unable to complete ${operation}. Check migration history or database state.`,
),
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.');
}
};
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.');
},
};

View File

@ -1,183 +1,176 @@
import { UserProfile, RowModelRequest, RowRequestSchema } from "@nice/common"; import { UserProfile, RowModelRequest, RowRequestSchema } from '@nice/common';
import { RowModelService } from "./row-model.service"; import { RowModelService } from './row-model.service';
import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder"; import { isFieldCondition, LogicalCondition, SQLBuilder } from './sql-builder';
import EventBus from "@server/utils/event-bus"; import EventBus from '@server/utils/event-bus';
import supejson from "superjson-cjs" import supejson from 'superjson-cjs';
import { deleteByPattern } from "@server/utils/redis/utils"; import { deleteByPattern } from '@server/utils/redis/utils';
import { redis } from "@server/utils/redis/redis.service"; import { redis } from '@server/utils/redis/redis.service';
import { z } from "zod"; import { z } from 'zod';
export class RowCacheService extends RowModelService { export class RowCacheService extends RowModelService {
constructor(tableName: string, private enableCache: boolean = true) { constructor(
super(tableName) tableName: string,
if (this.enableCache) { private enableCache: boolean = true,
EventBus.on("dataChanged", async ({ type, data }) => { ) {
if (type === tableName) { super(tableName);
const dataArray = Array.isArray(data) ? data : [data]; if (this.enableCache) {
for (const item of dataArray) { EventBus.on('dataChanged', async ({ type, data }) => {
try { if (type === tableName) {
if (item.id) { const dataArray = Array.isArray(data) ? data : [data];
this.invalidateRowCacheById(item.id) for (const item of dataArray) {
} try {
if (item.parentId) { if (item.id) {
this.invalidateRowCacheById(item.parentId) this.invalidateRowCacheById(item.id);
} }
} catch (err) { if (item.parentId) {
console.error(`Error deleting cache for type ${tableName}:`, err); 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<any> {
// 如果没有id直接返回原数据
if (!data?.id) return data;
// 如果未启用缓存,直接处理并返回数据
if (!this.enableCache) {
return this.processDataWithPermissions(data, staff);
} }
private async invalidateRowCacheById(id: string) { const key = this.getRowCacheKey(data.id);
if (!this.enableCache) return; try {
const pattern = this.getRowCacheKey(id); // 尝试从缓存获取数据
await deleteByPattern(pattern); const cachedData = await this.getCachedData(key, staff);
} // 如果缓存命中,直接返回
createJoinSql(request?: RowModelRequest): string[] { if (cachedData) return cachedData;
return [] // 处理数据并缓存
} const processedData = await this.processDataWithPermissions(data, staff);
protected async getRowRelation(args: { data: any, staff?: UserProfile }) { await redis.set(key, supejson.stringify(processedData));
return args.data; return processedData;
} } catch (err) {
protected async setResPermissions( this.logger.error('Error in getRowDto:', err);
data: any, throw err;
staff?: UserProfile,
) {
return data
}
protected async getRowDto(
data: any,
staff?: UserProfile,
): Promise<any> {
// 如果没有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;
}
} }
}
private async getCachedData( private async getCachedData(
key: string, key: string,
staff?: UserProfile staff?: UserProfile,
): Promise<any | null> { ): Promise<any | null> {
const cachedDataStr = await redis.get(key); const cachedDataStr = await redis.get(key);
if (!cachedDataStr) return null; if (!cachedDataStr) return null;
const cachedData = supejson.parse(cachedDataStr) as any; const cachedData = supejson.parse(cachedDataStr) as any;
if (!cachedData?.id) return null; if (!cachedData?.id) return null;
return staff return staff ? this.setResPermissions(cachedData, staff) : cachedData;
? this.setResPermissions(cachedData, staff) }
: cachedData;
}
private async processDataWithPermissions( private async processDataWithPermissions(
data: any, data: any,
staff?: UserProfile staff?: UserProfile,
): Promise<any> { ): Promise<any> {
// 处理权限 // 处理权限
const permData = staff const permData = staff ? await this.setResPermissions(data, staff) : data;
? await this.setResPermissions(data, staff) // 获取关联数据
: data; return this.getRowRelation({ data: permData, staff });
// 获取关联数据 }
return this.getRowRelation({ data: permData, staff });
}
protected createGetRowsFilters( protected createGetRowsFilters(
request: z.infer<typeof RowRequestSchema>, request: z.infer<typeof RowRequestSchema>,
staff?: UserProfile, staff?: UserProfile,
) { ) {
const condition = super.createGetRowsFilters(request); const condition = super.createGetRowsFilters(request);
if (isFieldCondition(condition)) return {}; if (isFieldCondition(condition)) return {};
const baseCondition: LogicalCondition[] = [ const baseCondition: LogicalCondition[] = [
{ {
field: `${this.tableName}.deleted_at`, field: `${this.tableName}.deleted_at`,
op: 'blank', op: 'blank',
type: 'date', type: 'date',
}, },
]; ];
condition.AND = [...baseCondition, ...condition.AND]; condition.AND = [...baseCondition, ...condition.AND];
return condition; return condition;
} }
createUnGroupingRowSelect(request?: RowModelRequest): string[] { createUnGroupingRowSelect(request?: RowModelRequest): string[] {
return [ return [
`${this.tableName}.id AS id`, `${this.tableName}.id AS id`,
SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`), SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
]; ];
} }
protected createGroupingRowSelect( protected createGroupingRowSelect(
request: RowModelRequest, request: RowModelRequest,
wrapperSql: boolean, wrapperSql: boolean,
): string[] { ): string[] {
const colsToSelect = super.createGroupingRowSelect(request, wrapperSql); const colsToSelect = super.createGroupingRowSelect(request, wrapperSql);
return colsToSelect.concat([ return colsToSelect.concat([
SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`), SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
]); ]);
} }
protected async getRowsSqlWrapper( protected async getRowsSqlWrapper(
sql: string, sql: string,
request?: RowModelRequest, request?: RowModelRequest,
staff?: UserProfile, staff?: UserProfile,
): Promise<string> { ): Promise<string> {
const groupingSql = SQLBuilder.join([ const groupingSql = SQLBuilder.join([
SQLBuilder.select([ SQLBuilder.select([
...this.createGroupingRowSelect(request, true), ...this.createGroupingRowSelect(request, true),
`${this.tableName}.id AS id`, `${this.tableName}.id AS id`,
]), ]),
SQLBuilder.from(this.tableName), SQLBuilder.from(this.tableName),
SQLBuilder.join(this.createJoinSql(request)), SQLBuilder.join(this.createJoinSql(request)),
SQLBuilder.where(this.createGetRowsFilters(request, staff)), SQLBuilder.where(this.createGetRowsFilters(request, staff)),
]); ]);
const { rowGroupCols, valueCols, groupKeys } = request; const { rowGroupCols, valueCols, groupKeys } = request;
if (this.isDoingGroup(request)) { if (this.isDoingGroup(request)) {
const rowGroupCol = rowGroupCols[groupKeys.length]; const rowGroupCol = rowGroupCols[groupKeys.length];
const groupByField = rowGroupCol?.field?.replace('.', '_'); const groupByField = rowGroupCol?.field?.replace('.', '_');
return SQLBuilder.join([ return SQLBuilder.join([
SQLBuilder.select([ SQLBuilder.select([
groupByField, groupByField,
...super.createAggSqlForWrapper(request), ...super.createAggSqlForWrapper(request),
'COUNT(id) AS child_count', 'COUNT(id) AS child_count',
]), ]),
SQLBuilder.from(`(${groupingSql})`), SQLBuilder.from(`(${groupingSql})`),
SQLBuilder.where({ SQLBuilder.where({
field: 'row_num', field: 'row_num',
value: '1', value: '1',
op: 'equals', op: 'equals',
}), }),
SQLBuilder.groupBy([groupByField]), SQLBuilder.groupBy([groupByField]),
SQLBuilder.orderBy( SQLBuilder.orderBy(
this.getOrderByColumns(request).map((item) => item.replace('.', '_')), this.getOrderByColumns(request).map((item) => item.replace('.', '_')),
), ),
this.getLimitSql(request), this.getLimitSql(request),
]); ]);
} else } else
return SQLBuilder.join([ return SQLBuilder.join([
SQLBuilder.select(['*']), SQLBuilder.select(['*']),
SQLBuilder.from(`(${sql})`), SQLBuilder.from(`(${sql})`),
SQLBuilder.where({ SQLBuilder.where({
field: 'row_num', field: 'row_num',
value: '1', value: '1',
op: 'equals', op: 'equals',
}), }),
this.getLimitSql(request), this.getLimitSql(request),
]); ]);
// return super.getRowsSqlWrapper(sql, request) // return super.getRowsSqlWrapper(sql, request)
} }
} }

View File

@ -21,7 +21,7 @@ export abstract class RowModelService {
// 添加更多需要引号的关键词 // 添加更多需要引号的关键词
]); ]);
protected logger = new Logger(this.tableName); protected logger = new Logger(this.tableName);
protected constructor(protected tableName: string) { } protected constructor(protected tableName: string) {}
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> { protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
return row; return row;
} }
@ -140,11 +140,11 @@ export abstract class RowModelService {
private buildFilterConditions(filterModel: any): LogicalCondition[] { private buildFilterConditions(filterModel: any): LogicalCondition[] {
return filterModel return filterModel
? Object.entries(filterModel)?.map(([key, item]) => ? Object.entries(filterModel)?.map(([key, item]) =>
SQLBuilder.createFilterSql( SQLBuilder.createFilterSql(
key === 'ag-Grid-AutoColumn' ? 'name' : key, key === 'ag-Grid-AutoColumn' ? 'name' : key,
item, item,
), ),
) )
: []; : [];
} }
@ -160,7 +160,10 @@ export abstract class RowModelService {
const { rowGroupCols, valueCols, groupKeys } = request; const { rowGroupCols, valueCols, groupKeys } = request;
return valueCols.map( return valueCols.map(
(valueCol) => (valueCol) =>
`${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}`, `${valueCol.aggFunc}(${valueCol.field.replace(
'.',
'_',
)}) AS ${valueCol.field.split('.').join('_')}`,
); );
} }
protected createGroupingRowSelect( protected createGroupingRowSelect(
@ -179,7 +182,9 @@ export abstract class RowModelService {
colsToSelect.push( colsToSelect.push(
...valueCols.map( ...valueCols.map(
(valueCol) => (valueCol) =>
`${wrapperSql ? '' : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`, `${wrapperSql ? '' : valueCol.aggFunc}(${
valueCol.field
}) AS ${valueCol.field.replace('.', '_')}`,
), ),
); );
@ -286,7 +291,10 @@ export abstract class RowModelService {
protected buildAggSelect(valueCols: any[]): string[] { protected buildAggSelect(valueCols: any[]): string[] {
return valueCols.map( return valueCols.map(
(valueCol) => (valueCol) =>
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`, `${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace(
'.',
'_',
)}`,
); );
} }

View File

@ -1,138 +1,172 @@
export interface FieldCondition { export interface FieldCondition {
field: string; field: string;
op: OperatorType op: OperatorType;
type?: "text" | "number" | "date"; type?: 'text' | 'number' | 'date';
value?: any; value?: any;
valueTo?: any; valueTo?: any;
}; }
export type OperatorType = 'equals' | 'notEqual' | 'contains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank' | 'greaterThan' | 'lessThanOrEqual' | 'inRange' | 'lessThan' | 'greaterThan' | 'in'; export type OperatorType =
export type LogicalCondition = FieldCondition | { | 'equals'
AND?: LogicalCondition[]; | 'notEqual'
OR?: LogicalCondition[]; | '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 { export function isFieldCondition(
return (condition as FieldCondition).field !== undefined; condition: LogicalCondition,
): condition is FieldCondition {
return (condition as FieldCondition).field !== undefined;
} }
function buildCondition(condition: FieldCondition): string { function buildCondition(condition: FieldCondition): string {
const { field, op, value, type = "text", valueTo } = condition; const { field, op, value, type = 'text', valueTo } = condition;
switch (op) { switch (op) {
case 'equals': case 'equals':
return `${field} = '${value}'`; return `${field} = '${value}'`;
case 'notEqual': case 'notEqual':
return `${field} != '${value}'`; return `${field} != '${value}'`;
case 'contains': case 'contains':
return `${field} LIKE '%${value}%'`; return `${field} LIKE '%${value}%'`;
case 'startsWith': case 'startsWith':
return `${field} LIKE '${value}%'`; return `${field} LIKE '${value}%'`;
case 'endsWith': case 'endsWith':
return `${field} LIKE '%${value}'`; return `${field} LIKE '%${value}'`;
case 'blank': case 'blank':
if (type !== "date") if (type !== 'date') return `(${field} IS NULL OR ${field} = '')`;
return `(${field} IS NULL OR ${field} = '')`; else return `${field} IS NULL`;
else case 'notBlank':
return `${field} IS NULL`; if (type !== 'date') return `${field} IS NOT NULL AND ${field} != ''`;
case 'notBlank': else return `${field} IS NOT NULL`;
if (type !== 'date') case 'greaterThan':
return `${field} IS NOT NULL AND ${field} != ''`; return `${field} > '${value}'`;
else case 'lessThanOrEqual':
return `${field} IS NOT NULL`; return `${field} <= '${value}'`;
case 'greaterThan': case 'lessThan':
return `${field} > '${value}'`; return `${field} < '${value}'`;
case 'lessThanOrEqual': case 'greaterThan':
return `${field} <= '${value}'`; return `${field} > '${value}'`;
case 'lessThan': case 'inRange':
return `${field} < '${value}'`; return `${field} >= '${value}' AND ${field} <= '${valueTo}'`;
case 'greaterThan': case 'in':
return `${field} > '${value}'`; if (!value || (Array.isArray(value) && value.length === 0)) {
case 'inRange': // Return a condition that is always false if value is empty or an empty array
return `${field} >= '${value}' AND ${field} <= '${valueTo}'`; return '1 = 0';
case 'in': }
if (!value || (Array.isArray(value) && value.length === 0)) { return `${field} IN (${(value as any[])
// Return a condition that is always false if value is empty or an empty array .map((val) => `'${val}'`)
return '1 = 0'; .join(', ')})`;
} default:
return `${field} IN (${(value as any[]).map(val => `'${val}'`).join(', ')})`; return 'true'; // Default return for unmatched conditions
default: }
return 'true'; // Default return for unmatched conditions
}
} }
function buildLogicalCondition(logicalCondition: LogicalCondition): string { function buildLogicalCondition(logicalCondition: LogicalCondition): string {
if (isFieldCondition(logicalCondition)) { if (isFieldCondition(logicalCondition)) {
return buildCondition(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) { // Process OR conditions
const andParts = logicalCondition.AND if (logicalCondition.OR && logicalCondition.OR.length > 0) {
.map(c => buildLogicalCondition(c)) const orParts = logicalCondition.OR.map((c) =>
.filter(part => part !== ''); // Filter out empty conditions buildLogicalCondition(c),
if (andParts.length > 0) { ).filter((part) => part !== ''); // Filter out empty conditions
parts.push(`(${andParts.join(' AND ')})`); if (orParts.length > 0) {
} parts.push(`(${orParts.join(' OR ')})`);
} }
// Process OR conditions }
if (logicalCondition.OR && logicalCondition.OR.length > 0) { // Join AND and OR parts with an 'AND' if both are present
const orParts = logicalCondition.OR return parts.length > 1 ? parts.join(' AND ') : parts[0] || '';
.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] || '';
} }
export class SQLBuilder { export class SQLBuilder {
static select(fields: string[], distinctField?: string): string { static select(fields: string[], distinctField?: string): string {
const distinctClause = distinctField ? `DISTINCT ON (${distinctField}) ` : ""; const distinctClause = distinctField
return `SELECT ${distinctClause}${fields.join(", ")}`; ? `DISTINCT ON (${distinctField}) `
} : '';
static rowNumber(orderBy: string, partitionBy: string | null = null, alias: string = 'row_num'): string { return `SELECT ${distinctClause}${fields.join(', ')}`;
if (!orderBy) { }
throw new Error("orderBy 参数不能为空"); static rowNumber(
} orderBy: string,
partitionBy: string | null = null,
let partitionClause = ''; alias: string = 'row_num',
if (partitionBy) { ): string {
partitionClause = `PARTITION BY ${partitionBy} `; if (!orderBy) {
} throw new Error('orderBy 参数不能为空');
return `ROW_NUMBER() OVER (${partitionClause}ORDER BY ${orderBy}) AS ${alias}`;
}
static from(tableName: string): string {
return `FROM ${tableName}`;
} }
static where(conditions: LogicalCondition): string { let partitionClause = '';
const whereClause = buildLogicalCondition(conditions); if (partitionBy) {
return whereClause ? `WHERE ${whereClause}` : ""; partitionClause = `PARTITION BY ${partitionBy} `;
} }
static groupBy(columns: string[]): string { return `ROW_NUMBER() OVER (${partitionClause}ORDER BY ${orderBy}) AS ${alias}`;
return columns.length ? `GROUP BY ${columns.join(", ")}` : ""; }
} static from(tableName: string): string {
return `FROM ${tableName}`;
}
static orderBy(columns: string[]): string { static where(conditions: LogicalCondition): string {
return columns.length ? `ORDER BY ${columns.join(", ")}` : ""; const whereClause = buildLogicalCondition(conditions);
} return whereClause ? `WHERE ${whereClause}` : '';
}
static limit(pageSize: number, offset: number = 0): string { static groupBy(columns: string[]): string {
return `LIMIT ${pageSize + 1} OFFSET ${offset}`; return columns.length ? `GROUP BY ${columns.join(', ')}` : '';
} }
static join(clauses: string[]): string { static orderBy(columns: string[]): string {
return clauses.filter(Boolean).join(' '); return columns.length ? `ORDER BY ${columns.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)
} 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);
}
} }

View File

@ -1,9 +1,9 @@
import { Controller, UseGuards } from "@nestjs/common"; import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { DailyTrainService } from "./dailyTrain.service"; import { DailyTrainService } from './dailyTrain.service';
@Controller('train-content') @Controller('train-content')
export class DailyTrainController { export class DailyTrainController {
constructor(private readonly dailyTrainService: DailyTrainService) {} constructor(private readonly dailyTrainService: DailyTrainService) {}
//@UseGuards(AuthGuard) //@UseGuards(AuthGuard)
} }

View File

@ -5,11 +5,10 @@ import { DailyTrainController } from './dailyTrain.controller';
import { DailyTrainService } from './dailyTrain.service'; import { DailyTrainService } from './dailyTrain.service';
import { DailyTrainRouter } from './dailyTrain.router'; import { DailyTrainRouter } from './dailyTrain.router';
@Module({ @Module({
imports: [StaffModule], imports: [StaffModule],
controllers: [DailyTrainController], controllers: [DailyTrainController],
providers: [DailyTrainService,DailyTrainRouter,TrpcService], providers: [DailyTrainService, DailyTrainRouter, TrpcService],
exports: [DailyTrainService,DailyTrainRouter], exports: [DailyTrainService, DailyTrainRouter],
}) })
export class DailyTrainModule {} export class DailyTrainModule {}

View File

@ -1,16 +1,13 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { TrpcService } from "@server/trpc/trpc.service"; import { TrpcService } from '@server/trpc/trpc.service';
import { DailyTrainService } from "./dailyTrain.service"; import { DailyTrainService } from './dailyTrain.service';
@Injectable() @Injectable()
export class DailyTrainRouter { export class DailyTrainRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly dailyTrainService: DailyTrainService, private readonly dailyTrainService: DailyTrainService,
) { } ) {}
router = this.trpc.router({
})
router = this.trpc.router({});
} }

View File

@ -1,40 +1,37 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { BaseService } from "../base/base.service"; import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma, UserProfile } from "@nice/common"; import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
import { DefaultArgs } from "@prisma/client/runtime/library"; import { DefaultArgs } from '@prisma/client/runtime/library';
import EventBus, { CrudOperation } from "@server/utils/event-bus"; import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable() @Injectable()
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> { export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
constructor() { constructor() {
super(db,ObjectType.DAILY_TRAIN,true); super(db, ObjectType.DAILY_TRAIN, true);
} }
async create(args: Prisma.DailyTrainTimeCreateArgs) { async create(args: Prisma.DailyTrainTimeCreateArgs) {
console.log(args) console.log(args);
const result = await super.create(args) const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED,result) this.emitDataChanged(CrudOperation.CREATED, result);
return result return result;
} }
async update(args:Prisma.DailyTrainTimeUpdateArgs){ async update(args: Prisma.DailyTrainTimeUpdateArgs) {
const result = await super.update(args) const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED,result) this.emitDataChanged(CrudOperation.UPDATED, result);
return result return result;
} }
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
const result = await super.findMany(args);
return result;
}
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) { private emitDataChanged(operation: CrudOperation, data: any) {
const result = await super.findMany(args); EventBus.emit('dataChanged', {
return result; type: ObjectType.DAILY_TRAIN,
} operation,
data,
});
private emitDataChanged(operation: CrudOperation, data: any) { }
EventBus.emit('dataChanged', {
type:ObjectType.DAILY_TRAIN,
operation,
data,
});
}
} }

View File

@ -6,7 +6,7 @@ import { db } from '@nice/common';
@Controller('dept') @Controller('dept')
export class DepartmentController { export class DepartmentController {
constructor(private readonly deptService: DepartmentService) { } constructor(private readonly deptService: DepartmentService) {}
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@Get('get-detail') @Get('get-detail')
async getDepartmentDetails(@Query('dept-id') deptId: string) { async getDepartmentDetails(@Query('dept-id') deptId: string) {

View File

@ -6,8 +6,13 @@ import { DepartmentController } from './department.controller';
import { DepartmentRowService } from './department.row.service'; import { DepartmentRowService } from './department.row.service';
@Module({ @Module({
providers: [DepartmentService, DepartmentRouter, DepartmentRowService, TrpcService], providers: [
exports: [DepartmentService, DepartmentRouter], DepartmentService,
controllers: [DepartmentController], DepartmentRouter,
DepartmentRowService,
TrpcService,
],
exports: [DepartmentService, DepartmentRouter],
controllers: [DepartmentController],
}) })
export class DepartmentModule { } export class DepartmentModule {}

View File

@ -6,7 +6,7 @@ import { db, VisitType } from '@nice/common';
@Controller('message') @Controller('message')
export class MessageController { export class MessageController {
constructor(private readonly messageService: MessageService) { } constructor(private readonly messageService: MessageService) {}
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
@Get('find-last-one') @Get('find-last-one')
async findLastOne(@Query('staff-id') staffId: string) { async findLastOne(@Query('staff-id') staffId: string) {
@ -27,7 +27,7 @@ export class MessageController {
select: { select: {
title: true, title: true,
content: true, content: true,
url: true url: true,
}, },
}); });
@ -53,7 +53,7 @@ export class MessageController {
visits: { visits: {
none: { none: {
id: staffId, id: staffId,
type: VisitType.READED type: VisitType.READED,
}, },
}, },
receivers: { receivers: {
@ -92,7 +92,7 @@ export class MessageController {
visits: { visits: {
none: { none: {
id: staffId, id: staffId,
type: VisitType.READED type: VisitType.READED,
}, },
}, },
receivers: { receivers: {

View File

@ -11,4 +11,4 @@ import { MessageController } from './message.controller';
exports: [MessageService, MessageRouter], exports: [MessageService, MessageRouter],
controllers: [MessageController], controllers: [MessageController],
}) })
export class MessageModule { } export class MessageModule {}

View File

@ -3,15 +3,16 @@ import { TrpcService } from '@server/trpc/trpc.service';
import { MessageService } from './message.service'; import { MessageService } from './message.service';
import { Prisma } from '@nice/common'; import { Prisma } from '@nice/common';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> = z.any() const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> =
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any() z.any();
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any() const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any();
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any();
@Injectable() @Injectable()
export class MessageRouter { export class MessageRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly messageService: MessageService, private readonly messageService: MessageService,
) { } ) {}
router = this.trpc.router({ router = this.trpc.router({
create: this.trpc.procedure create: this.trpc.procedure
.input(MessageUncheckedCreateInputSchema) .input(MessageUncheckedCreateInputSchema)
@ -20,20 +21,21 @@ export class MessageRouter {
return await this.messageService.create({ data: input }, { staff }); return await this.messageService.create({ data: input }, { staff });
}), }),
findManyWithCursor: this.trpc.protectProcedure findManyWithCursor: this.trpc.protectProcedure
.input(z.object({ .input(
cursor: z.any().nullish(), z.object({
take: z.number().nullish(), cursor: z.any().nullish(),
where: MessageWhereInputSchema.nullish(), take: z.number().nullish(),
select: MessageSelectSchema.nullish() where: MessageWhereInputSchema.nullish(),
})) select: MessageSelectSchema.nullish(),
}),
)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const { staff } = ctx; const { staff } = ctx;
return await this.messageService.findManyWithCursor(input, staff); return await this.messageService.findManyWithCursor(input, staff);
}), }),
getUnreadCount: this.trpc.protectProcedure getUnreadCount: this.trpc.protectProcedure.query(async ({ ctx }) => {
.query(async ({ ctx }) => { const { staff } = ctx;
const { staff } = ctx; return await this.messageService.getUnreadCount(staff);
return await this.messageService.getUnreadCount(staff); }),
}) });
})
} }

View File

@ -8,26 +8,28 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
constructor() { constructor() {
super(db, ObjectType.MESSAGE); super(db, ObjectType.MESSAGE);
} }
async create(args: Prisma.MessageCreateArgs, params?: { tx?: Prisma.MessageDelegate, staff?: UserProfile }) { async create(
args: Prisma.MessageCreateArgs,
params?: { tx?: Prisma.MessageDelegate; staff?: UserProfile },
) {
args.data!.senderId = params?.staff?.id; args.data!.senderId = params?.staff?.id;
args.include = { args.include = {
receivers: { receivers: {
select: { id: true, registerToken: true, username: true } select: { id: true, registerToken: true, username: true },
} },
} };
const result = await super.create(args); const result = await super.create(args);
EventBus.emit("dataChanged", { EventBus.emit('dataChanged', {
type: ObjectType.MESSAGE, type: ObjectType.MESSAGE,
operation: CrudOperation.CREATED, operation: CrudOperation.CREATED,
data: result data: result,
}) });
return result return result;
} }
async findManyWithCursor( async findManyWithCursor(
args: Prisma.MessageFindManyArgs, args: Prisma.MessageFindManyArgs,
staff?: UserProfile, staff?: UserProfile,
) { ) {
return this.wrapResult(super.findManyWithCursor(args), async (result) => { return this.wrapResult(super.findManyWithCursor(args), async (result) => {
let { items } = result; let { items } = result;
await Promise.all( await Promise.all(
@ -46,12 +48,12 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
visits: { visits: {
none: { none: {
visitorId: staff?.id, visitorId: staff?.id,
type: VisitType.READED type: VisitType.READED,
} },
} },
} },
}) });
return count return count;
} }
} }

View File

@ -1,20 +1,18 @@
import { Message, UserProfile, VisitType, db } from "@nice/common" import { Message, UserProfile, VisitType, db } from '@nice/common';
export async function setMessageRelation( export async function setMessageRelation(
data: Message, data: Message,
staff?: UserProfile, staff?: UserProfile,
): Promise<any> { ): Promise<any> {
const readed =
(await db.visit.count({
where: {
messageId: data.id,
type: VisitType.READED,
visitorId: staff?.id,
},
})) > 0;
const readed = Object.assign(data, {
(await db.visit.count({ readed,
where: { });
messageId: data.id,
type: VisitType.READED,
visitorId: staff?.id,
},
})) > 0;
Object.assign(data, {
readed
})
} }

View File

@ -1,9 +1,9 @@
import { Controller, UseGuards } from "@nestjs/common"; import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { DailyTrainService } from "./dailyTrain.service"; import { DailyTrainService } from './dailyTrain.service';
@Controller('train-content') @Controller('train-content')
export class DailyTrainController { export class DailyTrainController {
constructor(private readonly dailyTrainService: DailyTrainService) {} constructor(private readonly dailyTrainService: DailyTrainService) {}
//@UseGuards(AuthGuard) //@UseGuards(AuthGuard)
} }

View File

@ -5,11 +5,10 @@ import { DailyTrainController } from './dailyTrain.controller';
import { DailyTrainService } from './dailyTrain.service'; import { DailyTrainService } from './dailyTrain.service';
import { DailyTrainRouter } from './dailyTrain.router'; import { DailyTrainRouter } from './dailyTrain.router';
@Module({ @Module({
imports: [StaffModule], imports: [StaffModule],
controllers: [DailyTrainController], controllers: [DailyTrainController],
providers: [DailyTrainService,DailyTrainRouter,TrpcService], providers: [DailyTrainService, DailyTrainRouter, TrpcService],
exports: [DailyTrainService,DailyTrainRouter], exports: [DailyTrainService, DailyTrainRouter],
}) })
export class DailyTrainModule {} export class DailyTrainModule {}

View File

@ -1,16 +1,13 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { TrpcService } from "@server/trpc/trpc.service"; import { TrpcService } from '@server/trpc/trpc.service';
import { DailyTrainService } from "./dailyTrain.service"; import { DailyTrainService } from './dailyTrain.service';
@Injectable() @Injectable()
export class DailyTrainRouter { export class DailyTrainRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly dailyTrainService: DailyTrainService, private readonly dailyTrainService: DailyTrainService,
) { } ) {}
router = this.trpc.router({
})
router = this.trpc.router({});
} }

View File

@ -1,40 +1,37 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { BaseService } from "../base/base.service"; import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma, UserProfile } from "@nice/common"; import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
import { DefaultArgs } from "@prisma/client/runtime/library"; import { DefaultArgs } from '@prisma/client/runtime/library';
import EventBus, { CrudOperation } from "@server/utils/event-bus"; import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable() @Injectable()
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> { export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
constructor() { constructor() {
super(db,ObjectType.DAILY_TRAIN,true); super(db, ObjectType.DAILY_TRAIN, true);
} }
async create(args: Prisma.DailyTrainTimeCreateArgs) { async create(args: Prisma.DailyTrainTimeCreateArgs) {
console.log(args) console.log(args);
const result = await super.create(args) const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED,result) this.emitDataChanged(CrudOperation.CREATED, result);
return result return result;
} }
async update(args:Prisma.DailyTrainTimeUpdateArgs){ async update(args: Prisma.DailyTrainTimeUpdateArgs) {
const result = await super.update(args) const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED,result) this.emitDataChanged(CrudOperation.UPDATED, result);
return result return result;
} }
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
const result = await super.findMany(args);
return result;
}
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) { private emitDataChanged(operation: CrudOperation, data: any) {
const result = await super.findMany(args); EventBus.emit('dataChanged', {
return result; type: ObjectType.DAILY_TRAIN,
} operation,
data,
});
private emitDataChanged(operation: CrudOperation, data: any) { }
EventBus.emit('dataChanged', {
type:ObjectType.DAILY_TRAIN,
operation,
data,
});
}
} }

View File

@ -8,7 +8,13 @@ import { DepartmentModule } from '../department/department.module';
@Module({ @Module({
imports: [DepartmentModule], imports: [DepartmentModule],
providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter], providers: [
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter] RoleMapService,
RoleRouter,
TrpcService,
RoleService,
RoleMapRouter,
],
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter],
}) })
export class RoleMapModule { } export class RoleMapModule {}

View File

@ -3,86 +3,91 @@ import { TrpcService } from '@server/trpc/trpc.service';
import { Prisma, UpdateOrderSchema } from '@nice/common'; import { Prisma, UpdateOrderSchema } from '@nice/common';
import { RoleService } from './role.service'; import { RoleService } from './role.service';
import { z, ZodType } from 'zod'; import { z, ZodType } from 'zod';
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any() const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any();
const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any() const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any();
const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any() const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any();
const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any() const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any();
const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any() const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any();
const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any() const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any();
const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any() const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any();
const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any() const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any();
const RoleUpdateInputSchema: ZodType<Prisma.RoleUpdateInput> = z.any(); const RoleUpdateInputSchema: ZodType<Prisma.RoleUpdateInput> = z.any();
@Injectable() @Injectable()
export class RoleRouter { export class RoleRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly roleService: RoleService, private readonly roleService: RoleService,
) { } ) {}
router = this.trpc.router({ router = this.trpc.router({
create: this.trpc.protectProcedure create: this.trpc.protectProcedure
.input(RoleCreateArgsSchema) .input(RoleCreateArgsSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { staff } = ctx; const { staff } = ctx;
return await this.roleService.create(input, staff); return await this.roleService.create(input, staff);
}), }),
update: this.trpc.protectProcedure update: this.trpc.protectProcedure
.input(RoleUpdateArgsSchema) .input(RoleUpdateArgsSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const { staff } = ctx; const { staff } = ctx;
return await this.roleService.update(input, staff); return await this.roleService.update(input, staff);
}), }),
createMany: this.trpc.protectProcedure.input(z.array(RoleCreateManyInputSchema)) createMany: this.trpc.protectProcedure
.mutation(async ({ ctx, input }) => { .input(z.array(RoleCreateManyInputSchema))
const { staff } = ctx; .mutation(async ({ ctx, input }) => {
const { staff } = ctx;
return await this.roleService.createMany({ data: input }, staff); return await this.roleService.createMany({ data: input }, staff);
}), }),
softDeleteByIds: this.trpc.protectProcedure softDeleteByIds: this.trpc.protectProcedure
.input( .input(
z.object({ z.object({
ids: z.array(z.string()), ids: z.array(z.string()),
data: RoleUpdateInputSchema.optional() data: RoleUpdateInputSchema.optional(),
}), }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return await this.roleService.softDeleteByIds(input.ids, input.data); return await this.roleService.softDeleteByIds(input.ids, input.data);
}), }),
findFirst: this.trpc.procedure findFirst: this.trpc.procedure
.input(RoleFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword .input(RoleFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
.query(async ({ input }) => { .query(async ({ input }) => {
return await this.roleService.findFirst(input); return await this.roleService.findFirst(input);
}), }),
updateOrder: this.trpc.protectProcedure updateOrder: this.trpc.protectProcedure
.input(UpdateOrderSchema) .input(UpdateOrderSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
return this.roleService.updateOrder(input); return this.roleService.updateOrder(input);
}), }),
findMany: this.trpc.procedure findMany: this.trpc.procedure
.input(RoleFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword .input(RoleFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
.query(async ({ input }) => { .query(async ({ input }) => {
return await this.roleService.findMany(input); return await this.roleService.findMany(input);
}), }),
findManyWithCursor: this.trpc.protectProcedure findManyWithCursor: this.trpc.protectProcedure
.input(z.object({ .input(
cursor: z.any().nullish(), z.object({
take: z.number().optional(), cursor: z.any().nullish(),
where: RoleWhereInputSchema.optional(), take: z.number().optional(),
select: RoleSelectSchema.optional() where: RoleWhereInputSchema.optional(),
})) select: RoleSelectSchema.optional(),
.query(async ({ ctx, input }) => { }),
const { staff } = ctx; )
return await this.roleService.findManyWithCursor(input); .query(async ({ ctx, input }) => {
}), const { staff } = ctx;
findManyWithPagination: this.trpc.procedure return await this.roleService.findManyWithCursor(input);
.input(z.object({ }),
page: z.number(), findManyWithPagination: this.trpc.procedure
pageSize: z.number().optional(), .input(
where: RoleWhereInputSchema.optional(), z.object({
select: RoleSelectSchema.optional() page: z.number(),
})) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword pageSize: z.number().optional(),
.query(async ({ input }) => { where: RoleWhereInputSchema.optional(),
return await this.roleService.findManyWithPagination(input); select: RoleSelectSchema.optional(),
}), }),
}); ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
.query(async ({ input }) => {
return await this.roleService.findManyWithPagination(input);
}),
});
} }

View File

@ -1,47 +1,59 @@
import { db, ObjectType, RowModelRequest, RowRequestSchema, UserProfile } from "@nice/common"; import {
import { RowCacheService } from "../base/row-cache.service"; db,
import { isFieldCondition, LogicalCondition } from "../base/sql-builder"; ObjectType,
import { z } from "zod"; RowModelRequest,
RowRequestSchema,
UserProfile,
} from '@nice/common';
import { RowCacheService } from '../base/row-cache.service';
import { isFieldCondition, LogicalCondition } from '../base/sql-builder';
import { z } from 'zod';
export class RoleRowService extends RowCacheService { export class RoleRowService extends RowCacheService {
protected createGetRowsFilters( protected createGetRowsFilters(
request: z.infer<typeof RowRequestSchema>, request: z.infer<typeof RowRequestSchema>,
staff?: UserProfile staff?: UserProfile,
) { ) {
const condition = super.createGetRowsFilters(request) const condition = super.createGetRowsFilters(request);
if (isFieldCondition(condition)) if (isFieldCondition(condition)) return {};
return {} const baseModelCondition: LogicalCondition[] = [
const baseModelCondition: LogicalCondition[] = [{ {
field: `${this.tableName}.deleted_at`, field: `${this.tableName}.deleted_at`,
op: "blank", op: 'blank',
type: "date" type: 'date',
}] },
condition.AND = [...baseModelCondition, ...condition.AND!] ];
return condition condition.AND = [...baseModelCondition, ...condition.AND!];
} return condition;
createUnGroupingRowSelect(): string[] { }
return [ createUnGroupingRowSelect(): string[] {
`${this.tableName}.id AS id`, return [
`${this.tableName}.name AS name`, `${this.tableName}.id AS id`,
`${this.tableName}.system AS system`, `${this.tableName}.name AS name`,
`${this.tableName}.permissions AS permissions` `${this.tableName}.system AS system`,
]; `${this.tableName}.permissions AS permissions`,
} ];
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> { }
if (!data.id) protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
return data if (!data.id) return data;
const roleMaps = await db.roleMap.findMany({ const roleMaps = await db.roleMap.findMany({
where: { where: {
roleId: data.id roleId: data.id,
} },
}) });
const deptIds = roleMaps.filter(item => item.objectType === ObjectType.DEPARTMENT).map(roleMap => roleMap.objectId) const deptIds = roleMaps
const staffIds = roleMaps.filter(item => item.objectType === ObjectType.STAFF).map(roleMap => roleMap.objectId) .filter((item) => item.objectType === ObjectType.DEPARTMENT)
const depts = await db.department.findMany({ where: { id: { in: deptIds } } }) .map((roleMap) => roleMap.objectId);
const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } }) const staffIds = roleMaps
const result = { ...data, depts, staffs } .filter((item) => item.objectType === ObjectType.STAFF)
return result .map((roleMap) => roleMap.objectId);
} const depts = await db.department.findMany({
createJoinSql(request?: RowModelRequest): string[] { where: { id: { in: deptIds } },
return []; });
} const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } });
const result = { ...data, depts, staffs };
return result;
}
createJoinSql(request?: RowModelRequest): string[] {
return [];
}
} }

View File

@ -1,9 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
import { import { ObjectType, RoleMapMethodSchema } from '@nice/common';
ObjectType,
RoleMapMethodSchema,
} from '@nice/common';
import { RoleMapService } from './rolemap.service'; import { RoleMapService } from './rolemap.service';
@Injectable() @Injectable()
@ -11,7 +8,7 @@ export class RoleMapRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly roleMapService: RoleMapService, private readonly roleMapService: RoleMapService,
) { } ) {}
router = this.trpc.router({ router = this.trpc.router({
deleteAllRolesForObject: this.trpc.protectProcedure deleteAllRolesForObject: this.trpc.protectProcedure
.input(RoleMapMethodSchema.deleteWithObject) .input(RoleMapMethodSchema.deleteWithObject)

View File

@ -64,10 +64,7 @@ export class RoleMapService extends RowModelService {
return condition; return condition;
} }
protected async getRowDto( protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
row: any,
staff?: UserProfile,
): Promise<any> {
if (!row.id) return row; if (!row.id) return row;
return row; return row;
} }
@ -126,15 +123,17 @@ export class RoleMapService extends RowModelService {
data: roleMaps, data: roleMaps,
}); });
}); });
const wrapResult = Promise.all(result.map(async item => { const wrapResult = Promise.all(
const staff = await db.staff.findMany({ result.map(async (item) => {
include: { department: true }, const staff = await db.staff.findMany({
where: { include: { department: true },
id: item.objectId where: {
} id: item.objectId,
}) },
return { ...item, staff } });
})) return { ...item, staff };
}),
);
return wrapResult; return wrapResult;
} }
async addRoleForObjects( async addRoleForObjects(
@ -187,11 +186,11 @@ export class RoleMapService extends RowModelService {
{ objectId: staffId, objectType: ObjectType.STAFF }, { objectId: staffId, objectType: ObjectType.STAFF },
...(deptId || ancestorDeptIds.length > 0 ...(deptId || ancestorDeptIds.length > 0
? [ ? [
{ {
objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) }, objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) },
objectType: ObjectType.DEPARTMENT, objectType: ObjectType.DEPARTMENT,
}, },
] ]
: []), : []),
]; ];
// Helper function to fetch roles based on domain ID. // 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))); // const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
return { items, totalCount }; return { items, totalCount };
} }
async getStaffsNotMap(data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>) { async getStaffsNotMap(
data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>,
) {
const { domainId, roleId } = data; const { domainId, roleId } = data;
let staffs = await db.staff.findMany({ let staffs = await db.staff.findMany({
where: { where: {
@ -300,7 +301,9 @@ export class RoleMapService extends RowModelService {
* @param data ID和域ID的数据 * @param data ID和域ID的数据
* @returns ID和员工ID列表 * @returns ID和员工ID列表
*/ */
async getRoleMapDetail(data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>) { async getRoleMapDetail(
data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>,
) {
const { roleId, domainId } = data; const { roleId, domainId } = data;
const res = await db.roleMap.findMany({ where: { roleId, domainId } }); const res = await db.roleMap.findMany({ where: { roleId, domainId } });

View File

@ -1,23 +1,24 @@
import path, { dirname } from "path"; import path, { dirname } from 'path';
import { FileMetadata, VideoMetadata, ResourceProcessor } from "../types"; import { FileMetadata, VideoMetadata, ResourceProcessor } from '../types';
import { Resource, ResourceStatus, db } from "@nice/common"; import { Resource, ResourceStatus, db } from '@nice/common';
import { Logger } from "@nestjs/common"; import { Logger } from '@nestjs/common';
import fs from 'fs/promises'; import fs from 'fs/promises';
export abstract class BaseProcessor implements ResourceProcessor { export abstract class BaseProcessor implements ResourceProcessor {
constructor() { } constructor() {}
protected logger = new Logger(BaseProcessor.name) protected logger = new Logger(BaseProcessor.name);
abstract process(resource: Resource): Promise<Resource> abstract process(resource: Resource): Promise<Resource>;
protected createOutputDir(filepath: string, subdirectory: string = 'assets'): string { protected createOutputDir(
const outputDir = path.join( filepath: string,
path.dirname(filepath), subdirectory: string = 'assets',
subdirectory, ): string {
); const outputDir = path.join(path.dirname(filepath), subdirectory);
fs.mkdir(outputDir, { recursive: true }).catch(err => this.logger.error(`Failed to create directory: ${err.message}`)); fs.mkdir(outputDir, { recursive: true }).catch((err) =>
this.logger.error(`Failed to create directory: ${err.message}`),
);
return outputDir; return outputDir;
}
}
} }
// //

View File

@ -4,7 +4,7 @@ import { ResourceService } from './resource.service';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
@Module({ @Module({
exports: [ResourceRouter, ResourceService], exports: [ResourceRouter, ResourceService],
providers: [ResourceRouter, ResourceService, TrpcService], providers: [ResourceRouter, ResourceService, TrpcService],
}) })
export class ResourceModule { } export class ResourceModule {}

View File

@ -1,4 +1,4 @@
import { Injectable ,Logger} from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { BaseService } from '../base/base.service'; import { BaseService } from '../base/base.service';
import { import {
@ -51,7 +51,9 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
where: { fileId }, where: { fileId },
data: { fileName }, data: { fileName },
}); });
this.logger.log(`更新了现有记录的文件名 "${fileName}" 到文件 ${fileId}`); this.logger.log(
`更新了现有记录的文件名 "${fileName}" 到文件 ${fileId}`,
);
} else { } else {
// 如果记录不存在,创建新记录 // 如果记录不存在,创建新记录
await db.shareCode.create({ await db.shareCode.create({
@ -63,7 +65,9 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
isUsed: false, isUsed: false,
}, },
}); });
this.logger.log(`创建了新记录并保存文件名 "${fileName}" 到文件 ${fileId}`); this.logger.log(
`创建了新记录并保存文件名 "${fileName}" 到文件 ${fileId}`,
);
} }
} catch (error) { } catch (error) {
this.logger.error(`保存文件名失败文件ID: ${fileId}`, error); this.logger.error(`保存文件名失败文件ID: ${fileId}`, error);

View File

@ -1,55 +1,57 @@
import { Resource } from "@nice/common"; import { Resource } from '@nice/common';
export interface ResourceProcessor { export interface ResourceProcessor {
process(resource: Resource): Promise<any> process(resource: Resource): Promise<any>;
} }
export interface ProcessResult { export interface ProcessResult {
success: boolean success: boolean;
resource: Resource resource: Resource;
error?: Error error?: Error;
} }
export interface BaseMetadata { export interface BaseMetadata {
size: number size: number;
filetype: string filetype: string;
filename: string filename: string;
extension: string extension: string;
modifiedAt: Date modifiedAt: Date;
} }
/** /**
* *
*/ */
export interface ImageMetadata { export interface ImageMetadata {
width: number; // 图片宽度(px) width: number; // 图片宽度(px)
height: number; // 图片高度(px) height: number; // 图片高度(px)
compressedUrl?: string; compressedUrl?: string;
orientation?: number; // EXIF方向信息 orientation?: number; // EXIF方向信息
space?: string; // 色彩空间 (如: RGB, CMYK) space?: string; // 色彩空间 (如: RGB, CMYK)
hasAlpha?: boolean; // 是否包含透明通道 hasAlpha?: boolean; // 是否包含透明通道
} }
/** /**
* *
*/ */
export interface VideoMetadata { export interface VideoMetadata {
width?: number; width?: number;
height?: number; height?: number;
duration?: number; duration?: number;
videoCodec?: string; videoCodec?: string;
audioCodec?: string; audioCodec?: string;
coverUrl?: string coverUrl?: string;
} }
/** /**
* *
*/ */
export interface AudioMetadata { export interface AudioMetadata {
duration: number; // 音频时长(秒) duration: number; // 音频时长(秒)
bitrate?: number; // 比特率(bps) bitrate?: number; // 比特率(bps)
sampleRate?: number; // 采样率(Hz) sampleRate?: number; // 采样率(Hz)
channels?: number; // 声道数 channels?: number; // 声道数
codec?: string; // 音频编码格式 codec?: string; // 音频编码格式
} }
export type FileMetadata = ImageMetadata &
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata VideoMetadata &
AudioMetadata &
BaseMetadata;

View File

@ -11,6 +11,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
imports: [DepartmentModule], imports: [DepartmentModule],
providers: [StaffService, StaffRouter, StaffRowService, TrpcService], providers: [StaffService, StaffRouter, StaffRowService, TrpcService],
exports: [StaffService, StaffRouter], exports: [StaffService, StaffRouter],
controllers: [StaffController, ], controllers: [StaffController],
}) })
export class StaffModule {} export class StaffModule {}

View File

@ -96,29 +96,33 @@ export class StaffRouter {
return await this.staffService.findUnique(input); return await this.staffService.findUnique(input);
}), }),
addCustomField: this.trpc.procedure addCustomField: this.trpc.procedure
.input(z.object({ .input(
name: z.string(), z.object({
label: z.string().optional(), name: z.string(),
type: z.string(), // text, number, date, select 等 label: z.string().optional(),
required: z.boolean().optional(), type: z.string(), // text, number, date, select 等
order: z.number().optional(), required: z.boolean().optional(),
options: z.any().optional(), // 对于选择类型字段的可选值 order: z.number().optional(),
group: z.string().optional(), // 字段分组 options: z.any().optional(), // 对于选择类型字段的可选值
})) group: z.string().optional(), // 字段分组
}),
)
.mutation(({ input }) => { .mutation(({ input }) => {
return this.staffService.addCustomField(input as any); return this.staffService.addCustomField(input as any);
}), }),
updateCustomField: this.trpc.procedure updateCustomField: this.trpc.procedure
.input(z.object({ .input(
id: z.string(), z.object({
name: z.string().optional(), id: z.string(),
label: z.string().optional(), name: z.string().optional(),
type: z.string().optional(), label: z.string().optional(),
required: z.boolean().optional(), type: z.string().optional(),
order: z.number().optional(), required: z.boolean().optional(),
options: z.any().optional(), order: z.number().optional(),
group: z.string().optional(), options: z.any().optional(),
})) group: z.string().optional(),
}),
)
.mutation(({ input }) => { .mutation(({ input }) => {
return this.staffService.updateCustomField(input as any); return this.staffService.updateCustomField(input as any);
}), }),
@ -127,19 +131,19 @@ export class StaffRouter {
.mutation(({ input }) => { .mutation(({ input }) => {
return this.staffService.deleteCustomField(input as any); return this.staffService.deleteCustomField(input as any);
}), }),
getCustomFields: this.trpc.procedure getCustomFields: this.trpc.procedure.query(() => {
.query(() => { return this.staffService.getCustomFields();
return this.staffService.getCustomFields(); }),
}),
setCustomFieldValue: this.trpc.procedure setCustomFieldValue: this.trpc.procedure
.input(z.object({ .input(
staffId: z.string(), z.object({
fieldId: z.string(), staffId: z.string(),
value: z.string().optional(), fieldId: z.string(),
})) value: z.string().optional(),
}),
)
.mutation(({ input }) => { .mutation(({ input }) => {
return this.staffService.setCustomFieldValue(input as any); return this.staffService.setCustomFieldValue(input as any);
}), }),
}); });
} }

View File

@ -180,10 +180,13 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
return { return {
...staff, ...staff,
fieldValues: fieldValues.reduce((acc, { field, value }) => ({ fieldValues: fieldValues.reduce(
...acc, (acc, { field, value }) => ({
[field.name]: value, ...acc,
}), {}), [field.name]: value,
}),
{},
),
}; };
} }
@ -231,7 +234,33 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
orderBy: { order: 'asc' }, orderBy: { order: 'asc' },
}); });
} }
async findManyWithPagination(params: {
page?: number;
pageSize?: number;
where?: Prisma.StaffWhereInput;
select?: Prisma.StaffSelect;
}) {
const { page = 1, pageSize = 10, where, select } = params;
const skip = (page - 1) * pageSize;
const [items, total] = await Promise.all([
this.prisma.staff.findMany({
skip,
take: pageSize,
where,
select,
}),
this.prisma.staff.count({ where }),
]);
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
async setCustomFieldValue(data: { async setCustomFieldValue(data: {
staffId: string; staffId: string;
fieldId: string; fieldId: string;
@ -243,7 +272,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
staffId_fieldId: { staffId_fieldId: {
staffId, staffId,
fieldId, fieldId,
} },
}, },
create: { create: {
staffId, staffId,
@ -255,6 +284,4 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
}, },
}); });
} }
} }

View File

@ -1,10 +1,10 @@
import { Controller, UseGuards } from "@nestjs/common"; import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
import { SystemLogService } from "./systemLog.service"; import { SystemLogService } from './systemLog.service';
@Controller('system-logs') @Controller('system-logs')
export class SystemLogController { export class SystemLogController {
constructor(private readonly systemLogService: SystemLogService) {} constructor(private readonly systemLogService: SystemLogService) {}
// @UseGuards(AuthGuard) // @UseGuards(AuthGuard)
// 控制器使用trpc路由不需要在这里定义API端点 // 控制器使用trpc路由不需要在这里定义API端点
} }

View File

@ -1,302 +1,312 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { TrpcService } from "@server/trpc/trpc.service"; import { TrpcService } from '@server/trpc/trpc.service';
import { SystemLogService } from "./systemLog.service"; import { SystemLogService } from './systemLog.service';
import { z, ZodType } from "zod"; import { z, ZodType } from 'zod';
import { Prisma } from "@nice/common"; import { Prisma } from '@nice/common';
// 定义Zod类型Schema // 定义Zod类型Schema
const SystemLogCreateArgsSchema: ZodType<Prisma.SystemLogCreateArgs> = z.any(); const SystemLogCreateArgsSchema: ZodType<Prisma.SystemLogCreateArgs> = z.any();
const SystemLogFindManyArgsSchema: ZodType<Prisma.SystemLogFindManyArgs> = z.any(); const SystemLogFindManyArgsSchema: ZodType<Prisma.SystemLogFindManyArgs> =
const SystemLogFindUniqueArgsSchema: ZodType<Prisma.SystemLogFindUniqueArgs> = z.any(); z.any();
const SystemLogFindUniqueArgsSchema: ZodType<Prisma.SystemLogFindUniqueArgs> =
z.any();
const SystemLogWhereInputSchema: ZodType<Prisma.SystemLogWhereInput> = z.any(); const SystemLogWhereInputSchema: ZodType<Prisma.SystemLogWhereInput> = z.any();
const SystemLogSelectSchema: ZodType<Prisma.SystemLogSelect> = z.any(); const SystemLogSelectSchema: ZodType<Prisma.SystemLogSelect> = z.any();
@Injectable() @Injectable()
export class SystemLogRouter { export class SystemLogRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly systemLogService: SystemLogService, private readonly systemLogService: SystemLogService,
) { } ) {}
router = this.trpc.router({ router = this.trpc.router({
// 创建日志 // 创建日志
create: this.trpc.procedure create: this.trpc.procedure
.input(z.object({ .input(
level: z.enum(['info', 'warning', 'error', 'debug']).default('info'), z.object({
module: z.string(), level: z.enum(['info', 'warning', 'error', 'debug']).default('info'),
action: z.string(), module: z.string(),
operatorId: z.string().optional(), action: z.string(),
ipAddress: z.string().optional(), operatorId: z.string().optional(),
targetId: z.string().optional(), ipAddress: z.string().optional(),
targetType: z.string().optional(), targetId: z.string().optional(),
targetName: z.string().optional(), targetType: z.string().optional(),
message: z.string().optional(), targetName: z.string().optional(),
details: z.any().optional(), message: z.string().optional(),
beforeData: z.any().optional(), details: z.any().optional(),
afterData: z.any().optional(), beforeData: z.any().optional(),
status: z.enum(['success', 'failure']).default('success'), afterData: z.any().optional(),
errorMessage: z.string().optional(), status: z.enum(['success', 'failure']).default('success'),
departmentId: z.string().optional(), errorMessage: z.string().optional(),
})) departmentId: z.string().optional(),
.mutation(async ({ input, ctx }) => { }),
const ctxIpAddress = ctx.ip; )
const operatorId = ctx.staff?.id; .mutation(async ({ input, ctx }) => {
const ctxIpAddress = ctx.ip;
const operatorId = ctx.staff?.id;
try { try {
return this.systemLogService.create({ return this.systemLogService.create({
data: { data: {
level: input.level, level: input.level,
module: input.module, module: input.module,
action: input.action, action: input.action,
operatorId: input.operatorId || operatorId, operatorId: input.operatorId || operatorId,
ipAddress: input.ipAddress || ctxIpAddress, ipAddress: input.ipAddress || ctxIpAddress,
targetId: input.targetId, targetId: input.targetId,
targetType: input.targetType, targetType: input.targetType,
targetName: input.targetName, targetName: input.targetName,
message: input.message, message: input.message,
details: input.details, details: input.details,
beforeData: input.beforeData, beforeData: input.beforeData,
afterData: input.afterData, afterData: input.afterData,
status: input.status, status: input.status,
errorMessage: input.errorMessage, errorMessage: input.errorMessage,
departmentId: input.departmentId, departmentId: input.departmentId,
} },
}); });
} catch (error) { } catch (error) {
console.error('Error creating system log:', error); console.error('Error creating system log:', error);
throw new Error('Failed to create system log'); throw new Error('Failed to create system log');
} }
}), }),
// 查询日志列表 // 查询日志列表
findMany: this.trpc.procedure findMany: this.trpc.procedure
.input(SystemLogFindManyArgsSchema) .input(SystemLogFindManyArgsSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
return this.systemLogService.findMany(input); return this.systemLogService.findMany(input);
}), }),
// 查询日志列表(带分页) - 保留原名 // 查询日志列表(带分页) - 保留原名
getLogs: this.trpc.procedure getLogs: this.trpc.procedure
.input(z.object({ .input(
page: z.number().default(1), z.object({
pageSize: z.number().default(20), page: z.number().default(1),
where: SystemLogWhereInputSchema.optional(), pageSize: z.number().default(20),
select: SystemLogSelectSchema.optional(), where: SystemLogWhereInputSchema.optional(),
})) select: SystemLogSelectSchema.optional(),
.query(async ({ input }) => { }),
try { )
const { page, pageSize, where = {}, select } = input; .query(async ({ input }) => {
try {
const { page, pageSize, where = {}, select } = input;
return await this.systemLogService.findManyWithPagination({ return await this.systemLogService.findManyWithPagination({
page, page,
pageSize, pageSize,
where, where,
...(select ? { select } : {}) ...(select ? { select } : {}),
}); });
} catch (error) { } catch (error) {
console.error('Error in getLogs:', error); console.error('Error in getLogs:', error);
// 返回空结果,避免崩溃 // 返回空结果,避免崩溃
return { return {
items: [], items: [],
total: 0, total: 0,
page: input.page, page: input.page,
pageSize: input.pageSize, pageSize: input.pageSize,
totalPages: 0 totalPages: 0,
}; };
} }
}), }),
// 查询日志列表(带分页) - 新名称 // 查询日志列表(带分页) - 新名称
findManyWithPagination: this.trpc.procedure findManyWithPagination: this.trpc.procedure
.input(z.object({ .input(
page: z.number().default(1), z.object({
pageSize: z.number().default(20), page: z.number().default(1),
where: SystemLogWhereInputSchema.optional(), pageSize: z.number().default(20),
select: SystemLogSelectSchema.optional(), where: SystemLogWhereInputSchema.optional(),
})) select: SystemLogSelectSchema.optional(),
.query(async ({ input }) => { }),
try { )
const { page, pageSize, where = {}, select } = input; .query(async ({ input }) => {
try {
const { page, pageSize, where = {}, select } = input;
return await this.systemLogService.findManyWithPagination({ return await this.systemLogService.findManyWithPagination({
page, page,
pageSize, pageSize,
where, where,
...(select ? { select } : {}) ...(select ? { select } : {}),
}); });
} catch (error) { } catch (error) {
console.error('Error in findManyWithPagination:', error); console.error('Error in findManyWithPagination:', error);
// 返回空结果,避免崩溃 // 返回空结果,避免崩溃
return { return {
items: [], items: [],
total: 0, total: 0,
page: input.page, page: input.page,
pageSize: input.pageSize, pageSize: input.pageSize,
totalPages: 0 totalPages: 0,
}; };
} }
}), }),
// 获取单个日志详情 // 获取单个日志详情
findUnique: this.trpc.procedure findUnique: this.trpc.procedure
.input(SystemLogFindUniqueArgsSchema) .input(SystemLogFindUniqueArgsSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
return this.systemLogService.findUnique(input); return this.systemLogService.findUnique(input);
}), }),
// 通过ID获取日志详情(简化版) // 通过ID获取日志详情(简化版)
findById: this.trpc.procedure findById: this.trpc.procedure.input(z.string()).query(async ({ input }) => {
.input(z.string()) return this.systemLogService.findUnique({
.query(async ({ input }) => { where: { id: input },
return this.systemLogService.findUnique({ include: {
where: { id: input }, operator: {
include: { select: {
operator: { id: true,
select: { username: true,
id: true, showname: true,
username: true, },
showname: true, },
} department: {
}, select: {
department: { id: true,
select: { name: true,
id: true, },
name: true, },
} },
} });
} }),
});
}),
// 记录人员操作日志的便捷方法 // 记录人员操作日志的便捷方法
logStaffAction: this.trpc.protectProcedure logStaffAction: this.trpc.protectProcedure
.input(z.object({ .input(
action: z.string(), z.object({
targetId: z.string(), action: z.string(),
targetName: z.string(), targetId: z.string(),
message: z.string().optional(), targetName: z.string(),
beforeData: z.any().optional(), message: z.string().optional(),
afterData: z.any().optional(), beforeData: z.any().optional(),
status: z.enum(['success', 'failure']).default('success'), afterData: z.any().optional(),
errorMessage: z.string().optional(), status: z.enum(['success', 'failure']).default('success'),
})) errorMessage: z.string().optional(),
.mutation(async ({ input, ctx }) => { }),
const ipAddress = ctx.ip; )
const operatorId = ctx.staff?.id; .mutation(async ({ input, ctx }) => {
const ipAddress = ctx.ip;
const operatorId = ctx.staff?.id;
try { try {
return this.systemLogService.create({ return this.systemLogService.create({
data: { data: {
level: input.status === 'success' ? 'info' : 'error', level: input.status === 'success' ? 'info' : 'error',
module: 'staff', module: 'staff',
action: input.action, action: input.action,
operatorId: operatorId, operatorId: operatorId,
ipAddress: ipAddress, ipAddress: ipAddress,
targetId: input.targetId, targetId: input.targetId,
targetType: 'staff', targetType: 'staff',
targetName: input.targetName, targetName: input.targetName,
message: input.message, message: input.message,
beforeData: input.beforeData, beforeData: input.beforeData,
afterData: input.afterData, afterData: input.afterData,
status: input.status, status: input.status,
errorMessage: input.errorMessage, errorMessage: input.errorMessage,
} },
}); });
} catch (error) { } catch (error) {
console.error('Error logging staff action:', error); console.error('Error logging staff action:', error);
throw new Error('Failed to log staff action'); throw new Error('Failed to log staff action');
} }
}), }),
// 高级搜索日志 // 高级搜索日志
searchLogs: this.trpc.procedure searchLogs: this.trpc.procedure
.input(z.object({ .input(
page: z.number().default(1), z.object({
pageSize: z.number().default(20), page: z.number().default(1),
level: z.enum(['info', 'warning', 'error', 'debug']).optional(), pageSize: z.number().default(20),
module: z.string().optional(), level: z.enum(['info', 'warning', 'error', 'debug']).optional(),
action: z.string().optional(), module: z.string().optional(),
operatorId: z.string().optional(), action: z.string().optional(),
targetId: z.string().optional(), operatorId: z.string().optional(),
targetType: z.string().optional(), targetId: z.string().optional(),
status: z.enum(['success', 'failure']).optional(), targetType: z.string().optional(),
startTime: z.string().optional(), status: z.enum(['success', 'failure']).optional(),
endTime: z.string().optional(), startTime: z.string().optional(),
keyword: z.string().optional(), endTime: z.string().optional(),
departmentId: z.string().optional(), keyword: z.string().optional(),
})) departmentId: z.string().optional(),
.query(async ({ input }) => { }),
console.log('Received input for searchLogs:', input); )
const where: Prisma.SystemLogWhereInput = {}; .query(async ({ input }) => {
console.log('Received input for searchLogs:', input);
const where: Prisma.SystemLogWhereInput = {};
if (input.level) where.level = input.level; if (input.level) where.level = input.level;
if (input.module) where.module = input.module; if (input.module) where.module = input.module;
if (input.action) where.action = input.action; if (input.action) where.action = input.action;
if (input.operatorId) where.operatorId = input.operatorId; if (input.operatorId) where.operatorId = input.operatorId;
if (input.targetId) where.targetId = input.targetId; if (input.targetId) where.targetId = input.targetId;
if (input.targetType) where.targetType = input.targetType; if (input.targetType) where.targetType = input.targetType;
if (input.status) where.status = input.status; if (input.status) where.status = input.status;
if (input.departmentId) where.departmentId = input.departmentId; if (input.departmentId) where.departmentId = input.departmentId;
// 时间范围查询 // 时间范围查询
if (input.startTime || input.endTime) { if (input.startTime || input.endTime) {
where.timestamp = {}; where.timestamp = {};
if (input.startTime) where.timestamp.gte = new Date(input.startTime); if (input.startTime) where.timestamp.gte = new Date(input.startTime);
if (input.endTime) where.timestamp.lte = new Date(input.endTime); if (input.endTime) where.timestamp.lte = new Date(input.endTime);
} }
// 关键词搜索 // 关键词搜索
if (input.keyword) { if (input.keyword) {
where.OR = [ where.OR = [
{ targetName: { contains: input.keyword } }, { targetName: { contains: input.keyword } },
{ action: { contains: input.keyword } }, { action: { contains: input.keyword } },
{ module: { contains: input.keyword } }, { module: { contains: input.keyword } },
{ errorMessage: { contains: input.keyword } }, { errorMessage: { contains: input.keyword } },
]; ];
} }
try { try {
const result = await this.systemLogService.findManyWithPagination({ const result = await this.systemLogService.findManyWithPagination({
page: input.page, page: input.page,
pageSize: input.pageSize, pageSize: input.pageSize,
where, where,
select: { select: {
id: true, id: true,
level: true, level: true,
module: true, module: true,
action: true, action: true,
timestamp: true, timestamp: true,
operatorId: true, operatorId: true,
ipAddress: true, ipAddress: true,
targetId: true, targetId: true,
targetType: true, targetType: true,
targetName: true, targetName: true,
details: true, details: true,
beforeData: true, beforeData: true,
afterData: true, afterData: true,
status: true, status: true,
errorMessage: true, errorMessage: true,
departmentId: true, departmentId: true,
operator: { operator: {
select: { select: {
id: true, id: true,
username: true, username: true,
showname: true, showname: true,
} },
}, },
department: { department: {
select: { select: {
id: true, id: true,
name: true, name: true,
} },
} },
} },
}); });
console.log('Search logs result:', result); console.log('Search logs result:', result);
return result; return result;
} catch (error) { } catch (error) {
console.error('Error in searchLogs:', error); console.error('Error in searchLogs:', error);
throw new Error('Failed to search logs'); throw new Error('Failed to search logs');
} }
}), }),
}) });
} }

View File

@ -1,134 +1,145 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { BaseService } from "../base/base.service"; import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma } from "@nice/common"; import { db, ObjectType, Prisma } from '@nice/common';
import EventBus, { CrudOperation } from "@server/utils/event-bus"; import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable() @Injectable()
export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> { export class SystemLogService extends BaseService<Prisma.SystemLogDelegate> {
protected readonly prismaClient: any; protected readonly prismaClient: any;
constructor() { constructor() {
super(db, ObjectType.SYSTEM_LOG, false); super(db, ObjectType.SYSTEM_LOG, false);
this.prismaClient = db; this.prismaClient = db;
}
async create(args: Prisma.SystemLogCreateArgs) {
// 确保消息字段有值
if (args.data && typeof args.data === 'object') {
const { level, module, action, targetName } = args.data as any;
const timestamp = new Date().toLocaleString();
const messagePrefix = level === 'error' ? '错误' : '';
// 添加默认消息格式 - 确保 message 字段存在
if (!args.data.message) {
args.data.message = `[${timestamp}] ${messagePrefix}${module || ''} ${
action || ''
}: ${targetName || ''}`;
}
} }
async create(args: Prisma.SystemLogCreateArgs) { const result = await super.create(args);
// 确保消息字段有值 this.emitDataChanged(CrudOperation.CREATED, result);
if (args.data && typeof args.data === 'object') { return result;
const { level, module, action, targetName } = args.data as any; }
const timestamp = new Date().toLocaleString();
const messagePrefix = level === 'error' ? '错误' : '';
// 添加默认消息格式 - 确保 message 字段存在 async findMany(
if (!args.data.message) { args: Prisma.SystemLogFindManyArgs,
args.data.message = `[${timestamp}] ${messagePrefix}${module || ''} ${action || ''}: ${targetName || ''}`; ): Promise<Prisma.SystemLogGetPayload<{}>[]> {
} return super.findMany(args); // 放弃分页结构
} }
const result = await super.create(args); async findManyWithPagination({
this.emitDataChanged(CrudOperation.CREATED, result); page = 1,
return result; pageSize = 20,
where = {},
...rest
}: any) {
const skip = (page - 1) * pageSize;
try {
const [items, total] = await Promise.all([
this.prismaClient.systemLog.findMany({
where,
skip,
take: pageSize,
orderBy: { timestamp: 'desc' },
...rest,
}),
this.prismaClient.systemLog.count({ where }),
]);
return {
items,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
} catch (error) {
console.error('Error in findManyWithPagination:', error);
throw error;
} }
}
async findMany(args: Prisma.SystemLogFindManyArgs): Promise<Prisma.SystemLogGetPayload<{}>[]> { async logStaffAction(
return super.findMany(args); // 放弃分页结构 action: string,
} operatorId: string | null,
ipAddress: string | null,
targetId: string,
targetName: string,
beforeData: any = null,
afterData: any = null,
status: 'success' | 'failure' = 'success',
errorMessage?: string,
) {
// 生成变更详情
const details =
beforeData && afterData
? this.generateChangeDetails(beforeData, afterData)
: {};
async findManyWithPagination({ page = 1, pageSize = 20, where = {}, ...rest }: any) { const timestamp = new Date().toLocaleString();
const skip = (page - 1) * pageSize; const messagePrefix = status === 'success' ? '' : '错误: ';
const message = `[${timestamp}] ${messagePrefix}用户 ${targetName}${action}`;
try { return this.create({
const [items, total] = await Promise.all([ data: {
this.prismaClient.systemLog.findMany({ level: status === 'success' ? 'info' : 'error',
where, module: '人员管理',
skip, action,
take: pageSize, operatorId,
orderBy: { timestamp: 'desc' }, ipAddress,
...rest targetId,
}), targetType: 'staff',
this.prismaClient.systemLog.count({ where }) targetName,
]); message,
details,
beforeData,
afterData,
status,
errorMessage,
},
});
}
return { /**
items, *
total, */
page, private generateChangeDetails(before: any, after: any) {
pageSize, if (!before || !after) return {};
totalPages: Math.ceil(total / pageSize)
};
} catch (error) {
console.error('Error in findManyWithPagination:', error);
throw error;
}
}
async logStaffAction( const changes: Record<string, { oldValue: any; newValue: any }> = {};
action: string,
operatorId: string | null,
ipAddress: string | null,
targetId: string,
targetName: string,
beforeData: any = null,
afterData: any = null,
status: 'success' | 'failure' = 'success',
errorMessage?: string
) {
// 生成变更详情
const details = beforeData && afterData
? this.generateChangeDetails(beforeData, afterData)
: {};
const timestamp = new Date().toLocaleString(); Object.keys(after).forEach((key) => {
const messagePrefix = status === 'success' ? '' : '错误: '; // 忽略一些不需要记录的字段
const message = `[${timestamp}] ${messagePrefix}用户 ${targetName}${action}`; if (['password', 'createdAt', 'updatedAt', 'deletedAt'].includes(key))
return;
return this.create({ if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
data: { changes[key] = {
level: status === 'success' ? 'info' : 'error', oldValue: before[key],
module: '人员管理', newValue: after[key],
action, };
operatorId, }
ipAddress, });
targetId, return { changes };
targetType: 'staff', }
targetName,
message,
details,
beforeData,
afterData,
status,
errorMessage,
}
});
}
/** private emitDataChanged(operation: CrudOperation, data: any) {
* EventBus.emit('dataChanged', {
*/ type: ObjectType.SYSTEM_LOG,
private generateChangeDetails(before: any, after: any) { operation,
if (!before || !after) return {}; data,
});
const changes: Record<string, { oldValue: any; newValue: any }> = {}; }
Object.keys(after).forEach(key => {
// 忽略一些不需要记录的字段
if (['password', 'createdAt', 'updatedAt', 'deletedAt'].includes(key)) return;
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
changes[key] = {
oldValue: before[key],
newValue: after[key]
};
}
});
return { changes };
}
private emitDataChanged(operation: CrudOperation, data: any) {
EventBus.emit('dataChanged', {
type: ObjectType.SYSTEM_LOG,
operation,
data,
});
}
} }

View File

@ -8,7 +8,7 @@ export class TaxonomyRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly taxonomyService: TaxonomyService, private readonly taxonomyService: TaxonomyService,
) { } ) {}
router = this.trpc.router({ router = this.trpc.router({
create: this.trpc.procedure create: this.trpc.procedure

View File

@ -13,4 +13,4 @@ import { TermRowService } from './term.row.service';
exports: [TermService, TermRouter], exports: [TermService, TermRouter],
controllers: [TermController], controllers: [TermController],
}) })
export class TermModule { } export class TermModule {}

View File

@ -1,9 +1,9 @@
import { Controller, UseGuards } from "@nestjs/common"; import { Controller, UseGuards } from '@nestjs/common';
import { TrainContentService } from "./trainContent.service"; import { TrainContentService } from './trainContent.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
@Controller('train-content') @Controller('train-content')
export class TrainContentController { export class TrainContentController {
constructor(private readonly trainContentService: TrainContentService) {} constructor(private readonly trainContentService: TrainContentService) {}
//@UseGuards(AuthGuard) //@UseGuards(AuthGuard)
} }

View File

@ -5,11 +5,10 @@ import { TrainContentController } from './trainContent.controller';
import { TrainContentRouter } from './trainContent.router'; import { TrainContentRouter } from './trainContent.router';
import { TrpcService } from '@server/trpc/trpc.service'; import { TrpcService } from '@server/trpc/trpc.service';
@Module({ @Module({
imports: [StaffModule], imports: [StaffModule],
controllers: [TrainContentController], controllers: [TrainContentController],
providers: [TrainContentService,TrainContentRouter,TrpcService], providers: [TrainContentService, TrainContentRouter, TrpcService],
exports: [TrainContentService,TrainContentRouter], exports: [TrainContentService, TrainContentRouter],
}) })
export class TrainContentModule {} export class TrainContentModule {}

View File

@ -1,32 +1,36 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { TrpcService } from "@server/trpc/trpc.service"; import { TrpcService } from '@server/trpc/trpc.service';
import { TrainContentService } from "./trainContent.service"; import { TrainContentService } from './trainContent.service';
import { z, ZodType } from "zod"; import { z, ZodType } from 'zod';
import { Prisma } from "@nice/common"; import { Prisma } from '@nice/common';
const TrainContentArgsSchema:ZodType<Prisma.TrainContentCreateArgs> = z.any() const TrainContentArgsSchema: ZodType<Prisma.TrainContentCreateArgs> = z.any();
const TrainContentUpdateArgsSchema:ZodType<Prisma.TrainContentUpdateArgs> = z.any() const TrainContentUpdateArgsSchema: ZodType<Prisma.TrainContentUpdateArgs> =
const TrainContentFindManyArgsSchema:ZodType<Prisma.TrainContentFindManyArgs> = z.any() z.any();
const TrainContentFindManyArgsSchema: ZodType<Prisma.TrainContentFindManyArgs> =
z.any();
@Injectable() @Injectable()
export class TrainContentRouter { export class TrainContentRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly trainContentService: TrainContentService, private readonly trainContentService: TrainContentService,
) { } ) {}
router = this.trpc.router({
create:this.trpc.procedure.input(TrainContentArgsSchema)
.mutation(async ({input})=>{
return this.trainContentService.create(input)
}),
update:this.trpc.procedure.input(TrainContentUpdateArgsSchema)
.mutation(async ({input})=>{
return this.trainContentService.update(input)
}),
findMany:this.trpc.procedure.input(TrainContentFindManyArgsSchema)
.query(async ({input})=>{
return this.trainContentService.findMany(input)
})
})
router = this.trpc.router({
create: this.trpc.procedure
.input(TrainContentArgsSchema)
.mutation(async ({ input }) => {
return this.trainContentService.create(input);
}),
update: this.trpc.procedure
.input(TrainContentUpdateArgsSchema)
.mutation(async ({ input }) => {
return this.trainContentService.update(input);
}),
findMany: this.trpc.procedure
.input(TrainContentFindManyArgsSchema)
.query(async ({ input }) => {
return this.trainContentService.findMany(input);
}),
});
} }

View File

@ -1,40 +1,37 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { BaseService } from "../base/base.service"; import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma, UserProfile } from "@nice/common"; import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
import { DefaultArgs } from "@prisma/client/runtime/library"; import { DefaultArgs } from '@prisma/client/runtime/library';
import EventBus, { CrudOperation } from "@server/utils/event-bus"; import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable() @Injectable()
export class TrainContentService extends BaseService<Prisma.TrainContentDelegate> { export class TrainContentService extends BaseService<Prisma.TrainContentDelegate> {
constructor() { constructor() {
super(db,ObjectType.TRAIN_CONTENT,true); super(db, ObjectType.TRAIN_CONTENT, true);
} }
async create(args: Prisma.TrainContentCreateArgs) { async create(args: Prisma.TrainContentCreateArgs) {
console.log(args) console.log(args);
const result = await super.create(args) const result = await super.create(args);
this.emitDataChanged(CrudOperation.CREATED,result) this.emitDataChanged(CrudOperation.CREATED, result);
return result return result;
} }
async update(args:Prisma.TrainContentUpdateArgs){ async update(args: Prisma.TrainContentUpdateArgs) {
const result = await super.update(args) const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED,result) this.emitDataChanged(CrudOperation.UPDATED, result);
return result return result;
} }
async findMany(args: Prisma.TrainContentFindManyArgs) {
const result = await super.findMany(args);
return result;
}
async findMany(args: Prisma.TrainContentFindManyArgs) { private emitDataChanged(operation: CrudOperation, data: any) {
const result = await super.findMany(args); EventBus.emit('dataChanged', {
return result; type: ObjectType.TRAIN_SITUATION,
} operation,
data,
});
private emitDataChanged(operation: CrudOperation, data: any) { }
EventBus.emit('dataChanged', {
type:ObjectType.TRAIN_SITUATION,
operation,
data,
});
}
} }

View File

@ -1,9 +1,9 @@
import { Controller, UseGuards } from "@nestjs/common"; import { Controller, UseGuards } from '@nestjs/common';
import { TrainSituationService } from "./trainSituation.service"; import { TrainSituationService } from './trainSituation.service';
import { AuthGuard } from '@server/auth/auth.guard'; import { AuthGuard } from '@server/auth/auth.guard';
@Controller('train-situation') @Controller('train-situation')
export class TrainSituationController { export class TrainSituationController {
constructor(private readonly trainContentService: TrainSituationService) {} constructor(private readonly trainContentService: TrainSituationService) {}
//@UseGuards(AuthGuard) //@UseGuards(AuthGuard)
} }

View File

@ -8,7 +8,7 @@ import { TrpcService } from '@server/trpc/trpc.service';
@Module({ @Module({
imports: [StaffModule], imports: [StaffModule],
controllers: [TrainSituationController], controllers: [TrainSituationController],
providers: [TrainSituationService,TrainSituationRouter,TrpcService], providers: [TrainSituationService, TrainSituationRouter, TrpcService],
exports: [TrainSituationService,TrainSituationRouter], exports: [TrainSituationService, TrainSituationRouter],
}) })
export class TrainSituationModule {} export class TrainSituationModule {}

View File

@ -1,45 +1,49 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { TrpcService } from "@server/trpc/trpc.service"; import { TrpcService } from '@server/trpc/trpc.service';
import { TrainSituationService } from "./trainSituation.service"; import { TrainSituationService } from './trainSituation.service';
import { z, ZodType } from "zod"; import { z, ZodType } from 'zod';
import { Prisma } from "@nice/common"; import { Prisma } from '@nice/common';
const TrainSituationArgsSchema:ZodType<Prisma.TrainSituationCreateArgs> = z.any() const TrainSituationArgsSchema: ZodType<Prisma.TrainSituationCreateArgs> =
const TrainSituationUpdateArgsSchema:ZodType<Prisma.TrainSituationUpdateArgs> = z.any() z.any();
const TrainSituationFindManyArgsSchema:ZodType<Prisma.TrainSituationFindManyArgs> = z.any() const TrainSituationUpdateArgsSchema: ZodType<Prisma.TrainSituationUpdateArgs> =
z.any();
const TrainSituationFindManyArgsSchema: ZodType<Prisma.TrainSituationFindManyArgs> =
z.any();
@Injectable() @Injectable()
export class TrainSituationRouter { export class TrainSituationRouter {
constructor( constructor(
private readonly trpc: TrpcService, private readonly trpc: TrpcService,
private readonly trainSituationService: TrainSituationService, private readonly trainSituationService: TrainSituationService,
) { } ) {}
router = this.trpc.router({
create:this.trpc.protectProcedure
.input(TrainSituationArgsSchema)
.mutation(async ({ input }) => {
return this.trainSituationService.create(input)
}),
update:this.trpc.protectProcedure
.input(TrainSituationUpdateArgsSchema)
.mutation(async ({ input }) => {
return this.trainSituationService.update(input)
}),
findMany:this.trpc.protectProcedure
.input(TrainSituationFindManyArgsSchema)
.query(async ({input}) =>{
return this.trainSituationService.findMany(input)
}),
findManyByDepId:this.trpc.protectProcedure
.input(z.object({
deptId: z.string().optional(),
domainId: z.string().optional(),
trainContentId: z.string().optional()
}))
.query(async ({input}) => {
return this.trainSituationService.findManyByDeptId(input)
})
})
router = this.trpc.router({
create: this.trpc.protectProcedure
.input(TrainSituationArgsSchema)
.mutation(async ({ input }) => {
return this.trainSituationService.create(input);
}),
update: this.trpc.protectProcedure
.input(TrainSituationUpdateArgsSchema)
.mutation(async ({ input }) => {
return this.trainSituationService.update(input);
}),
findMany: this.trpc.protectProcedure
.input(TrainSituationFindManyArgsSchema)
.query(async ({ input }) => {
return this.trainSituationService.findMany(input);
}),
findManyByDepId: this.trpc.protectProcedure
.input(
z.object({
deptId: z.string().optional(),
domainId: z.string().optional(),
trainContentId: z.string().optional(),
}),
)
.query(async ({ input }) => {
return this.trainSituationService.findManyByDeptId(input);
}),
});
} }

View File

@ -1,80 +1,80 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { BaseService } from "../base/base.service"; import { BaseService } from '../base/base.service';
import { db, ObjectType, Prisma, UserProfile } from "@nice/common"; import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
import EventBus, { CrudOperation } from "@server/utils/event-bus"; import EventBus, { CrudOperation } from '@server/utils/event-bus';
import { DefaultArgs } from "@prisma/client/runtime/library"; import { DefaultArgs } from '@prisma/client/runtime/library';
import { StaffService } from "../staff/staff.service"; import { StaffService } from '../staff/staff.service';
@Injectable() @Injectable()
export class TrainSituationService extends BaseService<Prisma.TrainSituationDelegate> { export class TrainSituationService extends BaseService<Prisma.TrainSituationDelegate> {
constructor(private readonly staffService:StaffService) { constructor(private readonly staffService: StaffService) {
super(db,ObjectType.TRAIN_SITUATION,false); super(db, ObjectType.TRAIN_SITUATION, false);
} }
// 创建培训情况 // 创建培训情况
async create( async create(args: Prisma.TrainSituationCreateArgs) {
args: Prisma.TrainSituationCreateArgs, console.log(args);
){ const result = await super.create(args);
console.log(args) this.emitDataChanged(CrudOperation.CREATED, result);
const result = await super.create(args); return result;
this.emitDataChanged(CrudOperation.CREATED, result); }
return result; // 更新培训情况
} async update(args: Prisma.TrainSituationUpdateArgs) {
// 更新培训情况 const result = await super.update(args);
async update( this.emitDataChanged(CrudOperation.UPDATED, result);
args: Prisma.TrainSituationUpdateArgs, return result;
){ }
const result = await super.update(args);
this.emitDataChanged(CrudOperation.UPDATED, result);
return result;
}
// 查找培训情况 // 查找培训情况
async findMany(args: Prisma.TrainSituationFindManyArgs): Promise<{ async findMany(args: Prisma.TrainSituationFindManyArgs): Promise<
id: string;
staffId: string;
trainContentId: string;
mustTrainTime: number;
alreadyTrainTime: number;
score: number;
}[]>
{ {
const result = await super.findMany(args); id: string;
return result; staffId: string;
} trainContentId: string;
// 查找某一单位所有人员的培训情况 mustTrainTime: number;
async findManyByDeptId(args:{ alreadyTrainTime: number;
deptId?:string, score: number;
domainId?:string, }[]
trainContentId?:string > {
}):Promise<{ const result = await super.findMany(args);
id: string; return result;
staffId: string; }
trainContentId: string; // 查找某一单位所有人员的培训情况
mustTrainTime: number; async findManyByDeptId(args: {
alreadyTrainTime: number; deptId?: string;
score: number; domainId?: string;
}[]> trainContentId?: string;
}): Promise<
{ {
const staffs = await this.staffService.findByDept({deptId:args.deptId,domainId:args.domainId}) id: string;
const result = await super.findMany({ staffId: string;
where:{ trainContentId: string;
staffId:{ mustTrainTime: number;
in:staffs.map(staff=>staff.id) alreadyTrainTime: number;
}, score: number;
...(args.trainContentId ? {trainContentId:args.trainContentId} : {}) }[]
} > {
}) const staffs = await this.staffService.findByDept({
return result deptId: args.deptId,
} domainId: args.domainId,
//async createDailyTrainTime() });
const result = await super.findMany({
where: {
staffId: {
in: staffs.map((staff) => staff.id),
},
...(args.trainContentId ? { trainContentId: args.trainContentId } : {}),
},
});
return result;
}
//async createDailyTrainTime()
// 发送数据变化事件 // 发送数据变化事件
private emitDataChanged(operation: CrudOperation, data: any) { private emitDataChanged(operation: CrudOperation, data: any) {
EventBus.emit('dataChanged', { EventBus.emit('dataChanged', {
type:ObjectType.TRAIN_SITUATION, type: ObjectType.TRAIN_SITUATION,
operation, operation,
data, data,
}); });
} }
} }

View File

@ -8,12 +8,7 @@ import { DepartmentModule } from '../department/department.module';
import { StaffModule } from '../staff/staff.module'; import { StaffModule } from '../staff/staff.module';
// import { TransformController } from './transform.controller'; // import { TransformController } from './transform.controller';
@Module({ @Module({
imports: [ imports: [DepartmentModule, StaffModule, TermModule, TaxonomyModule],
DepartmentModule,
StaffModule,
TermModule,
TaxonomyModule,
],
providers: [TransformService, TransformRouter, TrpcService], providers: [TransformService, TransformRouter, TrpcService],
exports: [TransformRouter, TransformService], exports: [TransformRouter, TransformService],
// controllers:[TransformController] // controllers:[TransformController]

View File

@ -5,6 +5,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
@Module({ @Module({
providers: [VisitService, VisitRouter, TrpcService], providers: [VisitService, VisitRouter, TrpcService],
exports: [VisitRouter] exports: [VisitRouter],
}) })
export class VisitModule { } export class VisitModule {}

View File

@ -1,205 +1,210 @@
import { WebSocketServer, WebSocket } from 'ws';
import { WebSocketServer, WebSocket } from "ws"; import { Logger } from '@nestjs/common';
import { Logger } from "@nestjs/common"; import { WebSocketServerConfig, WSClient, WebSocketType } from '../types';
import { WebSocketServerConfig, WSClient, WebSocketType } from "../types";
import { SocketMessage } from '@nice/common'; import { SocketMessage } from '@nice/common';
const DEFAULT_CONFIG: WebSocketServerConfig = { const DEFAULT_CONFIG: WebSocketServerConfig = {
pingInterval: 30000, pingInterval: 30000,
pingTimeout: 5000, pingTimeout: 5000,
debug: false, // 新增默认调试配置 debug: false, // 新增默认调试配置
}; };
interface IWebSocketServer { interface IWebSocketServer {
start(): Promise<void>; start(): Promise<void>;
stop(): Promise<void>; stop(): Promise<void>;
broadcast(data: any): void; broadcast(data: any): void;
handleConnection(ws: WSClient): void; handleConnection(ws: WSClient): void;
handleDisconnection(ws: WSClient): void; handleDisconnection(ws: WSClient): void;
} }
export abstract class BaseWebSocketServer implements IWebSocketServer { export abstract class BaseWebSocketServer implements IWebSocketServer {
private _wss: WebSocketServer | null = null; private _wss: WebSocketServer | null = null;
protected clients: Set<WSClient> = new Set(); protected clients: Set<WSClient> = new Set();
protected timeouts: Map<WSClient, NodeJS.Timeout> = new Map(); protected timeouts: Map<WSClient, NodeJS.Timeout> = new Map();
protected pingIntervalId?: NodeJS.Timeout; protected pingIntervalId?: NodeJS.Timeout;
protected readonly logger = new Logger(this.constructor.name); protected readonly logger = new Logger(this.constructor.name);
protected readonly finalConfig: WebSocketServerConfig; protected readonly finalConfig: WebSocketServerConfig;
private userClientMap: Map<string, WSClient> = new Map(); private userClientMap: Map<string, WSClient> = new Map();
constructor( constructor(protected readonly config: Partial<WebSocketServerConfig> = {}) {
protected readonly config: Partial<WebSocketServerConfig> = {} this.finalConfig = {
) { ...DEFAULT_CONFIG,
this.finalConfig = { ...config,
...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) { public getClientCount() {
this.logger.debug(message, ...optionalParams); return this.clients.size;
} }
} // 暴露 WebSocketServer 实例的只读访问
public getClientCount() { public get wss(): WebSocketServer | null {
return this.clients.size return this._wss;
} }
// 暴露 WebSocketServer 实例的只读访问
public get wss(): WebSocketServer | null { // 内部使用的 setter
return this._wss; 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<void> {
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<void> {
if (this.pingIntervalId) {
clearInterval(this.pingIntervalId);
this.pingIntervalId = undefined;
} }
// 内部使用的 setter this.clients.forEach((client) => client.close());
protected set wss(value: WebSocketServer | null) { this.clients.clear();
this._wss = value; 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 { public broadcast(data: SocketMessage): void {
return this.finalConfig.path || `/${this.serverType}`; 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<void> { const roomClientsCount = this.getRoomClientsCount(ws.roomId);
if (this._wss) await this.stop(); this.debugLog(`
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<void> {
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(`
[${this.serverType}] connected [${this.serverType}] connected
userId ${ws.userId} userId ${ws.userId}
roomId ${ws.roomId} roomId ${ws.roomId}
room clients ${roomClientsCount} room clients ${roomClientsCount}
total clients ${this.clients.size}`); 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 { const roomClientsCount = this.getRoomClientsCount(ws.roomId);
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); this.debugLog(`
this.debugLog(`
[${this.serverType}] disconnected [${this.serverType}] disconnected
userId ${ws.userId} userId ${ws.userId}
roomId ${ws.roomId} roomId ${ws.roomId}
room clients ${roomClientsCount} room clients ${roomClientsCount}
total clients ${this.clients.size}`); total clients ${this.clients.size}`);
} }
protected setupClientEvents(ws: WSClient): void { protected setupClientEvents(ws: WSClient): void {
ws.on('pong', () => this.handlePong(ws)) ws.on('pong', () => this.handlePong(ws))
.on('close', () => this.handleDisconnection(ws)) .on('close', () => this.handleDisconnection(ws))
.on('error', (error) => { .on('error', (error) => {
this.logger.error(`[${this.serverType}] client error on path ${this.serverPath}:`, error); this.logger.error(
this.handleDisconnection(ws); `[${this.serverType}] client error on path ${this.serverPath}:`,
}); error,
}
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
); );
} this.handleDisconnection(ws);
});
}
private pingClients(): void { private handlePong(ws: WSClient): void {
this.clients.forEach(ws => { ws.isAlive = true;
if (!ws.isAlive) return this.handleDisconnection(ws); const timeout = this.timeouts.get(ws);
if (timeout) {
ws.isAlive = false; clearTimeout(timeout);
ws.ping(); this.timeouts.delete(ws);
const timeout = setTimeout(
() => !ws.isAlive && this.handleDisconnection(ws),
this.finalConfig.pingTimeout
);
this.timeouts.set(ws, timeout);
});
} }
}
protected setupServerEvents(): void { private startPingInterval(): void {
if (!this._wss) return; this.pingIntervalId = setInterval(
this._wss () => this.pingClients(),
.on('connection', (ws: WSClient) => this.handleConnection(ws)) this.finalConfig.pingInterval,
.on('error', (error) => this.logger.error(`Server error on path ${this.serverPath}:`, error)); );
} }
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),
);
}
} }

View File

@ -8,12 +8,13 @@ import http from 'http';
import { parseInt as libParseInt } from 'lib0/number'; import { parseInt as libParseInt } from 'lib0/number';
import { WSSharedDoc } from './ws-shared-doc'; import { WSSharedDoc } from './ws-shared-doc';
/** /**
* URL配置, * URL配置,
* null * null
*/ */
const CALLBACK_URL = process.env.CALLBACK_URL ? new URL(process.env.CALLBACK_URL) : null; const CALLBACK_URL = process.env.CALLBACK_URL
? new URL(process.env.CALLBACK_URL)
: null;
/** /**
* , * ,
@ -25,7 +26,9 @@ const CALLBACK_TIMEOUT = libParseInt(process.env.CALLBACK_TIMEOUT || '5000');
* *
* CALLBACK_OBJECTS中解析JSON格式的配置 * CALLBACK_OBJECTS中解析JSON格式的配置
*/ */
const CALLBACK_OBJECTS: Record<string, string> = process.env.CALLBACK_OBJECTS ? JSON.parse(process.env.CALLBACK_OBJECTS) : {}; const CALLBACK_OBJECTS: Record<string, string> = process.env.CALLBACK_OBJECTS
? JSON.parse(process.env.CALLBACK_OBJECTS)
: {};
/** /**
* URL是否已配置的标志 * URL是否已配置的标志
@ -37,10 +40,13 @@ export const isCallbackSet = !!CALLBACK_URL;
*/ */
interface DataToSend { interface DataToSend {
room: string; // 房间/文档标识 room: string; // 房间/文档标识
data: Record<string, { data: Record<
type: string; // 数据类型 string,
content: any; // 数据内容 {
}>; type: string; // 数据类型
content: any; // 数据内容
}
>;
} }
/** /**
@ -59,25 +65,29 @@ type OriginType = any;
* @param origin - * @param origin -
* @param doc - * @param doc -
*/ */
export const callbackHandler = (update: UpdateType, origin: OriginType, doc: WSSharedDoc): void => { export const callbackHandler = (
update: UpdateType,
origin: OriginType,
doc: WSSharedDoc,
): void => {
// 获取文档名称作为房间标识 // 获取文档名称作为房间标识
const room = doc.name; const room = doc.name;
// 初始化要发送的数据对象 // 初始化要发送的数据对象
const dataToSend: DataToSend = { const dataToSend: DataToSend = {
room, room,
data: {} data: {},
}; };
// 获取所有需要监听的共享对象名称 // 获取所有需要监听的共享对象名称
const sharedObjectList = Object.keys(CALLBACK_OBJECTS); const sharedObjectList = Object.keys(CALLBACK_OBJECTS);
// 遍历所有共享对象,获取它们的最新内容 // 遍历所有共享对象,获取它们的最新内容
sharedObjectList.forEach(sharedObjectName => { sharedObjectList.forEach((sharedObjectName) => {
const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName]; const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName];
dataToSend.data[sharedObjectName] = { dataToSend.data[sharedObjectName] = {
type: sharedObjectType, type: sharedObjectType,
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON() content: getContent(sharedObjectName, sharedObjectType, doc).toJSON(),
}; };
}); });
@ -106,8 +116,8 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(dataString) 'Content-Length': Buffer.byteLength(dataString),
} },
}; };
// 创建HTTP请求 // 创建HTTP请求
@ -137,14 +147,24 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
* @param doc - * @param doc -
* @returns * @returns
*/ */
const getContent = (objName: string, objType: string, doc: WSSharedDoc): any => { const getContent = (
objName: string,
objType: string,
doc: WSSharedDoc,
): any => {
// 根据对象类型返回相应的共享对象 // 根据对象类型返回相应的共享对象
switch (objType) { switch (objType) {
case 'Array': return doc.getArray(objName); case 'Array':
case 'Map': return doc.getMap(objName); return doc.getArray(objName);
case 'Text': return doc.getText(objName); case 'Map':
case 'XmlFragment': return doc.getXmlFragment(objName); return doc.getMap(objName);
case 'XmlElement': return doc.getXmlElement(objName); case 'Text':
default: return {}; return doc.getText(objName);
case 'XmlFragment':
return doc.getXmlFragment(objName);
case 'XmlElement':
return doc.getXmlElement(objName);
default:
return {};
} }
}; };

View File

@ -3,6 +3,6 @@ import { YjsServer } from './yjs.server';
@Module({ @Module({
providers: [YjsServer], providers: [YjsServer],
exports: [YjsServer] exports: [YjsServer],
}) })
export class CollaborationModule { } export class CollaborationModule {}

View File

@ -23,7 +23,7 @@ if (typeof persistenceDir === 'string') {
ldb.storeUpdate(docName, update); ldb.storeUpdate(docName, update);
}); });
}, },
writeState: async (_docName, _ydoc) => { }, writeState: async (_docName, _ydoc) => {},
}; };
} }

View File

@ -1,5 +1,4 @@
export interface ConnectionOptions { export interface ConnectionOptions {
docName: string; docName: string;
gc: boolean; gc: boolean;
} }

View File

@ -1,158 +1,187 @@
import { readSyncMessage } from '@nice/common'; import { readSyncMessage } from '@nice/common';
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates, writeSyncStep1, writeUpdate } from '@nice/common'; import {
applyAwarenessUpdate,
Awareness,
encodeAwarenessUpdate,
removeAwarenessStates,
writeSyncStep1,
writeUpdate,
} from '@nice/common';
import * as encoding from 'lib0/encoding'; import * as encoding from 'lib0/encoding';
import * as decoding from 'lib0/decoding'; import * as decoding from 'lib0/decoding';
import * as Y from "yjs" import * as Y from 'yjs';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { getPersistence, setPersistence } from './persistence'; import { getPersistence, setPersistence } from './persistence';
import { callbackHandler, isCallbackSet } from './callback'; import { callbackHandler, isCallbackSet } from './callback';
import { WebSocket } from "ws"; import { WebSocket } from 'ws';
import { YMessageType } from '@nice/common'; import { YMessageType } from '@nice/common';
import { WSClient } from '../types'; import { WSClient } from '../types';
export const docs = new Map<string, WSSharedDoc>(); export const docs = new Map<string, WSSharedDoc>();
export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000'); export const CALLBACK_DEBOUNCE_WAIT = parseInt(
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000'); process.env.CALLBACK_DEBOUNCE_WAIT || '2000',
);
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(
process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000',
);
export const getYDoc = (docname: string, gc = true): WSSharedDoc => { export const getYDoc = (docname: string, gc = true): WSSharedDoc => {
return docs.get(docname) || createYDoc(docname, gc); return docs.get(docname) || createYDoc(docname, gc);
}; };
const createYDoc = (docname: string, gc: boolean): WSSharedDoc => { const createYDoc = (docname: string, gc: boolean): WSSharedDoc => {
const doc = new WSSharedDoc(docname, gc); const doc = new WSSharedDoc(docname, gc);
docs.set(docname, doc); docs.set(docname, doc);
return doc; return doc;
}; };
export const send = (doc: WSSharedDoc, conn: WebSocket, m: Uint8Array) => { export const send = (doc: WSSharedDoc, conn: WebSocket, m: Uint8Array) => {
if (conn.readyState !== WebSocket.OPEN) { if (conn.readyState !== WebSocket.OPEN) {
closeConn(doc, conn); closeConn(doc, conn);
return; return;
} }
try { try {
conn.send(m, {}, err => { err != null && closeConn(doc, conn) }); conn.send(m, {}, (err) => {
} catch (e) { err != null && closeConn(doc, conn);
closeConn(doc, conn); });
} } catch (e) {
closeConn(doc, conn);
}
}; };
export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => { export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => {
if (doc.conns.has(conn)) { if (doc.conns.has(conn)) {
const controlledIds = doc.conns.get(conn) as Set<number>; const controlledIds = doc.conns.get(conn) as Set<number>;
doc.conns.delete(conn); doc.conns.delete(conn);
removeAwarenessStates( removeAwarenessStates(doc.awareness, Array.from(controlledIds), null);
doc.awareness,
Array.from(controlledIds),
null
);
if (doc.conns.size === 0 && getPersistence() !== null) { if (doc.conns.size === 0 && getPersistence() !== null) {
getPersistence()?.writeState(doc.name, doc).then(() => { getPersistence()
doc.destroy(); ?.writeState(doc.name, doc)
}); .then(() => {
docs.delete(doc.name); doc.destroy();
} });
docs.delete(doc.name);
} }
conn.close(); }
conn.close();
}; };
export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8Array) => { export const messageListener = (
try { conn: WSClient,
const encoder = encoding.createEncoder(); doc: WSSharedDoc,
const decoder = decoding.createDecoder(message); message: Uint8Array,
const messageType = decoding.readVarUint(decoder); ) => {
switch (messageType) { try {
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) => {
const encoder = encoding.createEncoder(); const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, YMessageType.Sync); const decoder = decoding.createDecoder(message);
writeUpdate(encoder, update); const messageType = decoding.readVarUint(decoder);
const message = encoding.toUint8Array(encoder); switch (messageType) {
doc.conns.forEach((_, conn) => send(doc, conn, message)); 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<void> = (_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<void> = (_ydoc) =>
Promise.resolve();
export const setContentInitializor = (f: (ydoc: Y.Doc) => Promise<void>) => { export const setContentInitializor = (f: (ydoc: Y.Doc) => Promise<void>) => {
contentInitializor = f; contentInitializor = f;
}; };
export class WSSharedDoc extends Y.Doc { export class WSSharedDoc extends Y.Doc {
name: string; name: string;
conns: Map<WebSocket, Set<number>>; conns: Map<WebSocket, Set<number>>;
awareness: Awareness; awareness: Awareness;
whenInitialized: Promise<void>; whenInitialized: Promise<void>;
constructor(name: string, gc: boolean) { constructor(name: string, gc: boolean) {
super({ gc }); super({ gc });
this.name = name; this.name = name;
this.conns = new Map(); this.conns = new Map();
this.awareness = new Awareness(this); this.awareness = new Awareness(this);
this.awareness.setLocalState(null); this.awareness.setLocalState(null);
const awarenessUpdateHandler = ({ const awarenessUpdateHandler = (
added, {
updated, added,
removed updated,
}: { removed,
added: number[], }: {
updated: number[], added: number[];
removed: number[] updated: number[];
}, conn: WebSocket) => { removed: number[];
const changedClients = added.concat(updated, removed); },
if (changedClients.length === 0) return conn: WebSocket,
if (conn !== null) { ) => {
const connControlledIDs = this.conns.get(conn) as Set<number>; const changedClients = added.concat(updated, removed);
if (connControlledIDs !== undefined) { if (changedClients.length === 0) return;
added.forEach(clientID => { connControlledIDs.add(clientID); }); if (conn !== null) {
removed.forEach(clientID => { connControlledIDs.delete(clientID); }); const connControlledIDs = this.conns.get(conn) as Set<number>;
} if (connControlledIDs !== undefined) {
} added.forEach((clientID) => {
connControlledIDs.add(clientID);
const encoder = encoding.createEncoder(); });
encoding.writeVarUint(encoder, YMessageType.Awareness); removed.forEach((clientID) => {
encoding.writeVarUint8Array( connControlledIDs.delete(clientID);
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); 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);
}
} }

View File

@ -1,85 +1,117 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { WebSocketType, WSClient } from "../types"; import { WebSocketType, WSClient } from '../types';
import { BaseWebSocketServer } from "../base/base-websocket-server"; import { BaseWebSocketServer } from '../base/base-websocket-server';
import { encoding } from "lib0"; import { encoding } from 'lib0';
import { YMessageType, writeSyncStep1, encodeAwarenessUpdate } from "@nice/common"; import {
import { getYDoc, closeConn, WSSharedDoc, messageListener, send } from "./ws-shared-doc"; YMessageType,
writeSyncStep1,
encodeAwarenessUpdate,
} from '@nice/common';
import {
getYDoc,
closeConn,
WSSharedDoc,
messageListener,
send,
} from './ws-shared-doc';
@Injectable() @Injectable()
export class YjsServer extends BaseWebSocketServer { export class YjsServer extends BaseWebSocketServer {
public get serverType(): WebSocketType { public get serverType(): WebSocketType {
return WebSocketType.YJS; return WebSocketType.YJS;
} }
public override handleConnection( public override handleConnection(connection: WSClient): void {
connection: WSClient super.handleConnection(connection);
): void { try {
super.handleConnection(connection) connection.binaryType = 'arraybuffer';
try { const doc = this.initializeDocument(connection, connection.roomId, true);
connection.binaryType = 'arraybuffer'; this.setupConnectionHandlers(connection, doc);
const doc = this.initializeDocument(connection, connection.roomId, true); this.sendInitialSync(connection, doc);
this.setupConnectionHandlers(connection, doc); } catch (error: any) {
this.sendInitialSync(connection, doc); this.logger.error(
} catch (error: any) { `Error in handleNewConnection: ${error.message}`,
this.logger.error(`Error in handleNewConnection: ${error.message}`, error.stack); error.stack,
connection.close(); );
} connection.close();
} }
}
private initializeDocument(conn: WSClient, docName: string, gc: boolean) { private initializeDocument(conn: WSClient, docName: string, gc: boolean) {
const doc = getYDoc(docName, gc); const doc = getYDoc(docName, gc);
doc.conns.set(conn, new Set()); doc.conns.set(conn, new Set());
return doc; return doc;
} }
private setupConnectionHandlers(connection: WSClient, doc: WSSharedDoc): void { private setupConnectionHandlers(
connection.on('message', (message: ArrayBuffer) => { connection: WSClient,
this.handleMessage(connection, doc, message); doc: WSSharedDoc,
}); ): void {
connection.on('close', () => { connection.on('message', (message: ArrayBuffer) => {
this.handleClose(doc, connection); this.handleMessage(connection, doc, message);
}); });
connection.on('error', (error) => { connection.on('close', () => {
this.logger.error(`WebSocket error for doc ${doc.name}: ${error.message}`, error.stack); this.handleClose(doc, connection);
closeConn(doc, connection); });
this.logger.warn(`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`); 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 { private handleClose(doc: WSSharedDoc, connection: WSClient): void {
try { try {
closeConn(doc, connection); closeConn(doc, connection);
} catch (error: any) { } catch (error: any) {
this.logger.error(`Error closing connection: ${error.message}`, error.stack); this.logger.error(
} `Error closing connection: ${error.message}`,
error.stack,
);
} }
private handleMessage(connection: WSClient, doc: WSSharedDoc, message: ArrayBuffer): void { }
try { private handleMessage(
messageListener(connection, doc, new Uint8Array(message)); connection: WSClient,
} catch (error: any) { doc: WSSharedDoc,
this.logger.error(`Error handling message: ${error.message}`, error.stack); 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); private sendInitialSync(connection: WSClient, doc: any): void {
this.sendAwarenessStates(connection, doc); this.sendSyncStep1(connection, doc);
} this.sendAwarenessStates(connection, doc);
private sendSyncStep1(connection: WSClient, doc: any): void { }
const encoder = encoding.createEncoder(); private sendSyncStep1(connection: WSClient, doc: any): void {
encoding.writeVarUint(encoder, YMessageType.Sync); const encoder = encoding.createEncoder();
writeSyncStep1(encoder, doc); encoding.writeVarUint(encoder, YMessageType.Sync);
send(doc, connection, encoding.toUint8Array(encoder)); writeSyncStep1(encoder, doc);
} send(doc, connection, encoding.toUint8Array(encoder));
private sendAwarenessStates(connection: WSClient, doc: WSSharedDoc): void { }
const awarenessStates = doc.awareness.getStates(); private sendAwarenessStates(connection: WSClient, doc: WSSharedDoc): void {
const awarenessStates = doc.awareness.getStates();
if (awarenessStates.size > 0) { if (awarenessStates.size > 0) {
const encoder = encoding.createEncoder(); const encoder = encoding.createEncoder();
encoding.writeVarUint(encoder, YMessageType.Awareness); encoding.writeVarUint(encoder, YMessageType.Awareness);
encoding.writeVarUint8Array( encoding.writeVarUint8Array(
encoder, encoder,
encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys())) encodeAwarenessUpdate(
); doc.awareness,
send(doc, connection, encoding.toUint8Array(encoder)); Array.from(awarenessStates.keys()),
} ),
);
send(doc, connection, encoding.toUint8Array(encoder));
} }
}
} }

View File

@ -1,9 +1,8 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { RealtimeServer } from './realtime.server'; import { RealtimeServer } from './realtime.server';
@Module({ @Module({
providers: [ RealtimeServer], providers: [RealtimeServer],
exports: [ RealtimeServer] exports: [RealtimeServer],
}) })
export class RealTimeModule { } export class RealTimeModule {}

View File

@ -1,25 +1,37 @@
import { Injectable, OnModuleInit } from "@nestjs/common"; import { Injectable, OnModuleInit } from '@nestjs/common';
import { WebSocketType } from "../types"; import { WebSocketType } from '../types';
import { BaseWebSocketServer } from "../base/base-websocket-server"; import { BaseWebSocketServer } from '../base/base-websocket-server';
import EventBus, { CrudOperation } from "@server/utils/event-bus"; import EventBus, { CrudOperation } from '@server/utils/event-bus';
import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nice/common"; import {
ObjectType,
SocketMsgType,
MessageDto,
PostDto,
PostType,
} from '@nice/common';
@Injectable() @Injectable()
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit { export class RealtimeServer
onModuleInit() { extends BaseWebSocketServer
EventBus.on("dataChanged", ({ data, type, operation }) => { implements OnModuleInit
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) { {
const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id) onModuleInit() {
this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } }) EventBus.on('dataChanged', ({ data, type, operation }) => {
} if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
const receiverIds = (data as Partial<MessageDto>).receivers.map(
(receiver) => receiver.id,
);
this.sendToUsers(receiverIds, {
type: SocketMsgType.NOTIFY,
payload: { objectType: ObjectType.MESSAGE },
});
}
if (type === ObjectType.POST) { if (type === ObjectType.POST) {
const post = data as Partial<PostDto> const post = data as Partial<PostDto>;
}
} });
}) }
public get serverType(): WebSocketType {
} return WebSocketType.REALTIME;
public get serverType(): WebSocketType { }
return WebSocketType.REALTIME;
}
} }

View File

@ -1,29 +1,29 @@
import { WebSocketServer, WebSocket } from "ws"; import { WebSocketServer, WebSocket } from 'ws';
// 类型定义 // 类型定义
export enum WebSocketType { export enum WebSocketType {
YJS = "yjs", YJS = 'yjs',
REALTIME = "realtime" REALTIME = 'realtime',
} }
export interface WebSocketServerConfig { export interface WebSocketServerConfig {
path?: string; path?: string;
pingInterval?: number; pingInterval?: number;
pingTimeout?: number; pingTimeout?: number;
debug?: boolean debug?: boolean;
} }
export interface ServerInstance { export interface ServerInstance {
wss: WebSocketServer | null; wss: WebSocketServer | null;
clients: Set<WSClient>; clients: Set<WSClient>;
pingIntervalId?: NodeJS.Timeout; pingIntervalId?: NodeJS.Timeout;
timeouts: Map<WebSocket, NodeJS.Timeout>; timeouts: Map<WebSocket, NodeJS.Timeout>;
} }
export interface WSClient extends WebSocket { export interface WSClient extends WebSocket {
isAlive?: boolean; isAlive?: boolean;
type?: WebSocketType; type?: WebSocketType;
userId?: string userId?: string;
origin?: string origin?: string;
roomId?: string roomId?: string;
} }

View File

@ -8,4 +8,4 @@ import { CollaborationModule } from './collaboration/collaboration.module';
providers: [WebSocketService], providers: [WebSocketService],
exports: [WebSocketService], exports: [WebSocketService],
}) })
export class WebSocketModule { } export class WebSocketModule {}

View File

@ -1,61 +1,61 @@
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from '@nestjs/common';
import { Server } from "http"; import { Server } from 'http';
import { WSClient } from "./types"; import { WSClient } from './types';
import { RealtimeServer } from "./realtime/realtime.server"; import { RealtimeServer } from './realtime/realtime.server';
import { YjsServer } from "./collaboration/yjs.server"; import { YjsServer } from './collaboration/yjs.server';
import { BaseWebSocketServer } from "./base/base-websocket-server"; import { BaseWebSocketServer } from './base/base-websocket-server';
@Injectable() @Injectable()
export class WebSocketService { export class WebSocketService {
private readonly logger = new Logger(WebSocketService.name); private readonly logger = new Logger(WebSocketService.name);
private readonly servers: BaseWebSocketServer[] = []; private readonly servers: BaseWebSocketServer[] = [];
constructor( constructor(
private realTimeServer: RealtimeServer, private realTimeServer: RealtimeServer,
private yjsServer: YjsServer private yjsServer: YjsServer,
) { ) {
this.servers.push(this.realTimeServer) this.servers.push(this.realTimeServer);
this.servers.push(this.yjsServer) this.servers.push(this.yjsServer);
}
public async initialize(httpServer: Server): Promise<void> {
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<void> { }
try { private setupUpgradeHandler(httpServer: Server): void {
await Promise.all(this.servers.map(server => server.start())); if (httpServer.listeners('upgrade').length) return;
this.setupUpgradeHandler(httpServer); httpServer.on('upgrade', async (request, socket, head) => {
} catch (error) { try {
this.logger.error('Failed to initialize:', error); const url = new URL(request.url!, `http://${request.headers.host}`);
throw error; 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 // 从URL查询参数中获取roomId和token
const urlParams = new URLSearchParams(url.search); const urlParams = new URLSearchParams(url.search);
const roomId = urlParams.get('roomId'); const roomId = urlParams.get('roomId');
const userId = urlParams.get('userId'); const userId = urlParams.get('userId');
const server = this.servers.find(server => { const server = this.servers.find((server) => {
const serverPathClean = server.serverPath.replace(/\/$/, ''); const serverPathClean = server.serverPath.replace(/\/$/, '');
const pathnameClean = pathname.replace(/\/$/, ''); const pathnameClean = pathname.replace(/\/$/, '');
return serverPathClean === pathnameClean; return serverPathClean === pathnameClean;
});
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();
}
}); });
}
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();
}
});
}
} }

View File

@ -52,8 +52,8 @@ export class GenDevService {
await this.generateStaffs(4); await this.generateStaffs(4);
//await this.generateTerms(2, 6); //await this.generateTerms(2, 6);
//await this.generateCourses(8); //await this.generateCourses(8);
await this.generateTrainContent(2,6) await this.generateTrainContent(2, 6);
await this.generateTrainSituations() await this.generateTrainSituations();
} catch (err) { } catch (err) {
this.logger.error(err); this.logger.error(err);
} }
@ -88,7 +88,9 @@ export class GenDevService {
this.domains.forEach((domain) => { this.domains.forEach((domain) => {
this.domainDepts[domain.id] = this.getAllChildDepartments(domain.id); this.domainDepts[domain.id] = this.getAllChildDepartments(domain.id);
this.logger.log( this.logger.log(
`Domain: ${domain.name} has ${this.domainDepts[domain.id].length} child departments`, `Domain: ${domain.name} has ${
this.domainDepts[domain.id].length
} child departments`,
); );
}); });
this.logger.log(`Completed: Generated ${this.depts.length} departments.`); this.logger.log(`Completed: Generated ${this.depts.length} departments.`);
@ -104,7 +106,9 @@ export class GenDevService {
if (currentDepth > maxDepth) return; if (currentDepth > maxDepth) return;
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const deptName = `${parentId?.slice(0, 6) || '根'}公司${currentDepth}-${i}`; const deptName = `${
parentId?.slice(0, 6) || '根'
}${currentDepth}-${i}`;
const newDept = await this.createDepartment( const newDept = await this.createDepartment(
deptName, deptName,
parentId, parentId,
@ -190,7 +194,9 @@ export class GenDevService {
}); });
for (const cate of cates) { for (const cate of cates) {
for (let i = 0; i < countPerCate; i++) { for (let i = 0; i < countPerCate; i++) {
const randomTitle = `${titleList[Math.floor(Math.random() * titleList.length)]} ${Math.random().toString(36).substring(7)}`; const randomTitle = `${
titleList[Math.floor(Math.random() * titleList.length)]
} ${Math.random().toString(36).substring(7)}`;
const randomLevelId = const randomLevelId =
levels[Math.floor(Math.random() * levels.length)].id; levels[Math.floor(Math.random() * levels.length)].id;
const randomDeptId = const randomDeptId =
@ -224,7 +230,9 @@ export class GenDevService {
this.deptStaffRecord[dept.id] = []; this.deptStaffRecord[dept.id] = [];
} }
for (let i = 0; i < countPerDept; i++) { for (let i = 0; i < countPerDept; i++) {
const username = `${dept.name}-S${staffsGenerated.toString().padStart(4, '0')}`; const username = `${dept.name}-S${staffsGenerated
.toString()
.padStart(4, '0')}`;
const staff = await this.staffService.create({ const staff = await this.staffService.create({
data: { data: {
showname: username, showname: username,
@ -328,7 +336,9 @@ export class GenDevService {
) => { ) => {
if (currentDepth > depth) return; if (currentDepth > depth) return;
for (let i = 0; i < nodesPerLevel; i++) { for (let i = 0; i < nodesPerLevel; i++) {
const name = `${taxonomySlug}-${domain?.name || 'public'}-${currentDepth}-${counter++} `; const name = `${taxonomySlug}-${
domain?.name || 'public'
}-${currentDepth}-${counter++} `;
const newTerm = await this.termService.create({ const newTerm = await this.termService.create({
data: { data: {
name, name,
@ -347,106 +357,113 @@ export class GenDevService {
} }
// 生成培训内容 // 生成培训内容
private async createTrainContent( private async createTrainContent(
type:string, type: string,
title:string, title: string,
parentId:string|null parentId: string | null,
){ ) {
const trainContent = await db.trainContent.create({ const trainContent = await db.trainContent.create({
data: { data: {
type, type,
title, title,
parentId parentId,
}, },
}); });
return trainContent; return trainContent;
} }
// 生成培训内容 // 生成培训内容
private async generateTrainContent(depth:number=3,count:number=6){ private async generateTrainContent(depth: number = 3, count: number = 6) {
if(this.counts.trainContentCount !== 0) return; if (this.counts.trainContentCount !== 0) return;
const totalTrainContent = this.calculateTotalTrainContent(depth,count) const totalTrainContent = this.calculateTotalTrainContent(depth, count);
this.logger.log("Start generating train content...") this.logger.log('Start generating train content...');
await this.generateSubTrainContent(null,1,depth,count,totalTrainContent) await this.generateSubTrainContent(
this.trainContents = await db.trainContent.findMany() null,
this.logger.log(`Completed: Generated ${this.trainContents.length} departments.`); 1,
depth,
count,
totalTrainContent,
);
this.trainContents = await db.trainContent.findMany();
this.logger.log(
`Completed: Generated ${this.trainContents.length} departments.`,
);
} }
// 生成培训内容子内容 // 生成培训内容子内容
private async generateSubTrainContent( private async generateSubTrainContent(
parentId: string | null, parentId: string | null,
currentDepth:number, currentDepth: number,
maxDepth:number, maxDepth: number,
count:number, count: number,
total:number, total: number,
){ ) {
if(currentDepth > maxDepth) return; if (currentDepth > maxDepth) return;
const contentType = [TrainContentType.SUBJECTS,TrainContentType.COURSE] const contentType = [TrainContentType.SUBJECTS, TrainContentType.COURSE];
for(let i = 0 ; i < count ; i++){ for (let i = 0; i < count; i++) {
const trainContentTitle = `${parentId?.slice(0,6) || '根'}公司${currentDepth}-${i}` const trainContentTitle = `${
parentId?.slice(0, 6) || '根'
}${currentDepth}-${i}`;
const newTrainContent = await this.createTrainContent( const newTrainContent = await this.createTrainContent(
contentType[currentDepth-1], contentType[currentDepth - 1],
trainContentTitle, trainContentTitle,
parentId parentId,
) );
this.trainContentGeneratedCount++; this.trainContentGeneratedCount++;
this.logger.log( this.logger.log(
`Generated ${this.trainContentGeneratedCount}/${total} train contents` `Generated ${this.trainContentGeneratedCount}/${total} train contents`,
) );
await this.generateSubTrainContent( await this.generateSubTrainContent(
newTrainContent.id, newTrainContent.id,
currentDepth+1, currentDepth + 1,
maxDepth, maxDepth,
count, count,
total total,
) );
} }
} }
private calculateTotalTrainContent(depth:number,count:number):number{ private calculateTotalTrainContent(depth: number, count: number): number {
let total = 0; let total = 0;
for(let i = 1 ; i<=depth;i++){ for (let i = 1; i <= depth; i++) {
total += Math.pow(count,i); total += Math.pow(count, i);
} }
return total; return total;
} }
private async createTrainSituation(staffId: string, trainContentId: string) {
private async createTrainSituation(staffId:string,trainContentId:string){
const trainSituation = await db.trainSituation.create({ const trainSituation = await db.trainSituation.create({
data:{ data: {
staffId, staffId,
trainContentId, trainContentId,
mustTrainTime:Math.floor(Math.random()*100), mustTrainTime: Math.floor(Math.random() * 100),
alreadyTrainTime:Math.floor(Math.random()*100), alreadyTrainTime: Math.floor(Math.random() * 100),
score:Math.floor(Math.random()*100), score: Math.floor(Math.random() * 100),
} },
}) });
return trainSituation return trainSituation;
} }
private async generateTrainSituations(probability: number = 0.1){ private async generateTrainSituations(probability: number = 0.1) {
this.logger.log("Start generating train situations...") this.logger.log('Start generating train situations...');
const allTrainContents = await db.trainContent.findMany(); const allTrainContents = await db.trainContent.findMany();
// 这里相当于两次遍历 找到没有parentID的即是 // 这里相当于两次遍历 找到没有parentID的即是
const leafNodes = allTrainContents.filter((item)=>item.parentId !== null) const leafNodes = allTrainContents.filter((item) => item.parentId !== null);
console.log(leafNodes.length) console.log(leafNodes.length);
const staffs = await db.staff.findMany() const staffs = await db.staff.findMany();
let situationCount = 0 let situationCount = 0;
const totalPossibleSituations = leafNodes.length * staffs.length const totalPossibleSituations = leafNodes.length * staffs.length;
for (const staff of staffs){ for (const staff of staffs) {
for(const leaf of leafNodes){ for (const leaf of leafNodes) {
if(Math.random() < probability){ if (Math.random() < probability) {
await this.createTrainSituation(staff.id,leaf.id) await this.createTrainSituation(staff.id, leaf.id);
situationCount++ situationCount++;
if (situationCount % 100 === 0) { if (situationCount % 100 === 0) {
this.logger.log( this.logger.log(`Generated ${situationCount} train situations`);
`Generated ${situationCount} train situations`
);
} }
} }
} }
} }
this.logger.log( this.logger.log(
`Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.` `Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.`,
); );
} }
} }

View File

@ -9,8 +9,15 @@ import { DepartmentModule } from '@server/models/department/department.module';
import { TermModule } from '@server/models/term/term.module'; import { TermModule } from '@server/models/term/term.module';
@Module({ @Module({
imports: [MinioModule, AuthModule, AppConfigModule, StaffModule, DepartmentModule, TermModule], imports: [
MinioModule,
AuthModule,
AppConfigModule,
StaffModule,
DepartmentModule,
TermModule,
],
providers: [InitService, GenDevService], providers: [InitService, GenDevService],
exports: [InitService] exports: [InitService],
}) })
export class InitModule { } export class InitModule {}

View File

@ -54,7 +54,7 @@ export class InitService {
}, },
}); });
this.logger.log(`Created new taxonomy: ${taxonomy.name}`); this.logger.log(`Created new taxonomy: ${taxonomy.name}`);
} else if(process.env.NODE_ENV === 'development'){ } else if (process.env.NODE_ENV === 'development') {
// Check for differences and update if necessary // Check for differences and update if necessary
const differences = Object.keys(taxonomy).filter( const differences = Object.keys(taxonomy).filter(
(key) => taxonomy[key] !== existingTaxonomy[key], (key) => taxonomy[key] !== existingTaxonomy[key],

View File

@ -16,7 +16,7 @@ export interface DevDataCounts {
export async function getCounts(): Promise<DevDataCounts> { export async function getCounts(): Promise<DevDataCounts> {
const counts = { const counts = {
deptCount: await db.department.count(), deptCount: await db.department.count(),
trainContentCount:await db.trainContent.count(), trainContentCount: await db.trainContent.count(),
staffCount: await db.staff.count(), staffCount: await db.staff.count(),
termCount: await db.term.count(), termCount: await db.term.count(),
courseCount: await db.post.count({ courseCount: await db.post.count({

View File

@ -3,8 +3,8 @@ import { ReminderService } from './reminder.service';
import { MessageModule } from '@server/models/message/message.module'; import { MessageModule } from '@server/models/message/message.module';
@Module({ @Module({
imports: [ MessageModule], imports: [MessageModule],
providers: [ReminderService], providers: [ReminderService],
exports: [ReminderService] exports: [ReminderService],
}) })
export class ReminderModule { } export class ReminderModule {}

View File

@ -15,67 +15,65 @@ import { MessageService } from '@server/models/message/message.service';
*/ */
@Injectable() @Injectable()
export class ReminderService { export class ReminderService {
/** /**
* *
* @private * @private
*/ */
private readonly logger = new Logger(ReminderService.name); private readonly logger = new Logger(ReminderService.name);
/** /**
* *
* @param messageService * @param messageService
*/ */
constructor(private readonly messageService: MessageService) { } constructor(private readonly messageService: MessageService) {}
/** /**
* *
* @param totalDays * @param totalDays
* @returns * @returns
*/ */
generateReminderTimes(totalDays: number): number[] { generateReminderTimes(totalDays: number): number[] {
// 如果总天数小于3天则不需要提醒 // 如果总天数小于3天则不需要提醒
if (totalDays < 3) return []; if (totalDays < 3) return [];
// 使用Set存储提醒时间点,避免重复 // 使用Set存储提醒时间点,避免重复
const reminders: Set<number> = new Set(); const reminders: Set<number> = new Set();
// 按照2的幂次方划分时间点 // 按照2的幂次方划分时间点
for (let i = 1; i <= totalDays / 2; i++) { for (let i = 1; i <= totalDays / 2; i++) {
reminders.add(Math.ceil(totalDays / Math.pow(2, i))); reminders.add(Math.ceil(totalDays / Math.pow(2, i)));
}
// 将Set转为数组并升序排序
return Array.from(reminders).sort((a, b) => a - b);
} }
// 将Set转为数组并升序排序
return Array.from(reminders).sort((a, b) => a - b);
}
/** /**
* *
* @param createdAt * @param createdAt
* @param deadline * @param deadline
* @returns * @returns
*/ */
shouldSendReminder(createdAt: Date, deadline: Date) { shouldSendReminder(createdAt: Date, deadline: Date) {
// 获取当前时间 // 获取当前时间
const now = dayjs(); const now = dayjs();
const end = dayjs(deadline); const end = dayjs(deadline);
// 计算总时间和剩余时间(天) // 计算总时间和剩余时间(天)
const totalTimeDays = end.diff(createdAt, 'day'); const totalTimeDays = end.diff(createdAt, 'day');
const timeLeftDays = end.diff(now, 'day'); const timeLeftDays = end.diff(now, 'day');
if (totalTimeDays > 1) { if (totalTimeDays > 1) {
// 获取提醒时间点 // 获取提醒时间点
const reminderTimes = this.generateReminderTimes(totalTimeDays); const reminderTimes = this.generateReminderTimes(totalTimeDays);
// 如果剩余时间在提醒时间点内,则需要提醒 // 如果剩余时间在提醒时间点内,则需要提醒
if (reminderTimes.includes(timeLeftDays)) { if (reminderTimes.includes(timeLeftDays)) {
return { shouldSend: true, timeLeft: timeLeftDays }; return { shouldSend: true, timeLeft: timeLeftDays };
} }
}
return { shouldSend: false, timeLeft: timeLeftDays };
} }
return { shouldSend: false, timeLeft: timeLeftDays };
}
/** /**
* *
*/ */
async remindDeadline() { async remindDeadline() {
this.logger.log('开始检查截止日期以发送提醒。'); this.logger.log('开始检查截止日期以发送提醒。');
}
}
} }

View File

@ -1,9 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service'; import { TasksService } from './tasks.service';
import { InitModule } from '@server/tasks/init/init.module'; import { InitModule } from '@server/tasks/init/init.module';
import { ReminderModule } from "@server/tasks/reminder/reminder.module" import { ReminderModule } from '@server/tasks/reminder/reminder.module';
@Module({ @Module({
imports: [InitModule, ReminderModule], imports: [InitModule, ReminderModule],
providers: [TasksService] providers: [TasksService],
}) })
export class TasksModule { } export class TasksModule {}

View File

@ -6,41 +6,47 @@ import { CronJob } from 'cron';
@Injectable() @Injectable()
export class TasksService implements OnModuleInit { export class TasksService implements OnModuleInit {
private readonly logger = new Logger(TasksService.name); private readonly logger = new Logger(TasksService.name);
constructor( constructor(
private readonly schedulerRegistry: SchedulerRegistry, private readonly schedulerRegistry: SchedulerRegistry,
private readonly initService: InitService, private readonly initService: InitService,
private readonly reminderService: ReminderService private readonly reminderService: ReminderService,
) { } ) {}
async onModuleInit() { async onModuleInit() {
this.logger.log('Main node launch'); this.logger.log('Main node launch');
await this.initService.init(); await this.initService.init();
this.logger.log('Initialization successful'); 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 { try {
const cronExpression = process.env.DEADLINE_CRON; await this.reminderService.remindDeadline();
if (!cronExpression) { this.logger.log('Reminder successfully processed');
throw new Error('DEADLINE_CRON environment variable is not set'); } catch (reminderErr) {
} this.logger.error(
'Error occurred while processing reminder',
const handleRemindJob = new CronJob(cronExpression, async () => { reminderErr,
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;
} }
});
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;
} }
}
} }

View File

@ -37,8 +37,8 @@ export class TrpcRouter {
private readonly visitor: VisitRouter, private readonly visitor: VisitRouter,
private readonly resource: ResourceRouter, private readonly resource: ResourceRouter,
private readonly trainContent: TrainContentRouter, private readonly trainContent: TrainContentRouter,
private readonly trainSituation:TrainSituationRouter, private readonly trainSituation: TrainSituationRouter,
private readonly dailyTrain:DailyTrainRouter, private readonly dailyTrain: DailyTrainRouter,
private readonly systemLogRouter: SystemLogRouter, private readonly systemLogRouter: SystemLogRouter,
) {} ) {}
getRouter() { getRouter() {
@ -57,9 +57,9 @@ export class TrpcRouter {
app_config: this.app_config.router, app_config: this.app_config.router,
visitor: this.visitor.router, visitor: this.visitor.router,
resource: this.resource.router, resource: this.resource.router,
trainContent:this.trainContent.router, trainContent: this.trainContent.router,
trainSituation:this.trainSituation.router, trainSituation: this.trainSituation.router,
dailyTrain:this.dailyTrain.router, dailyTrain: this.dailyTrain.router,
systemLog: this.systemLogRouter.router, systemLog: this.systemLogRouter.router,
}); });
wss: WebSocketServer = undefined; wss: WebSocketServer = undefined;

View File

@ -18,8 +18,9 @@ export class TrpcService {
ip: string; ip: string;
}> { }> {
const token = opts.req.headers.authorization?.split(' ')[1]; const token = opts.req.headers.authorization?.split(' ')[1];
const staff = const staff = await UserProfileService.instance.getUserProfileByToken(
await UserProfileService.instance.getUserProfileByToken(token); token,
);
const ip = getClientIp(opts.req); const ip = getClientIp(opts.req);
return { return {
staff: staff.staff, staff: staff.staff,

View File

@ -131,5 +131,4 @@ export class TusService implements OnModuleInit {
// console.log(req) // console.log(req)
return this.tusServer.handle(req, res); return this.tusServer.handle(req, res);
} }
} }

View File

@ -53,28 +53,28 @@ export class UploadController {
console.log('验证分享码结果:', shareCode); console.log('验证分享码结果:', shareCode);
if (!shareCode) { if (!shareCode) {
throw new NotFoundException('分享码无效或已过期'); throw new NotFoundException('分享码无效或已过期');
} }
// 获取文件信息 // 获取文件信息
const resource = await this.resourceService.findUnique({ const resource = await this.resourceService.findUnique({
where: { fileId: shareCode.fileId }, where: { fileId: shareCode.fileId },
}); });
console.log('获取到的资源信息:', resource); console.log('获取到的资源信息:', resource);
if (!resource) { if (!resource) {
throw new NotFoundException('文件不存在'); throw new NotFoundException('文件不存在');
} }
// 直接返回正确的数据结构 // 直接返回正确的数据结构
const response = { const response = {
fileId: shareCode.fileId, fileId: shareCode.fileId,
fileName:shareCode.fileName || 'downloaded_file', fileName: shareCode.fileName || 'downloaded_file',
code: shareCode.code, code: shareCode.code,
expiresAt: shareCode.expiresAt expiresAt: shareCode.expiresAt,
}; };
console.log('返回给前端的数据:', response); // 添加日志 console.log('返回给前端的数据:', response); // 添加日志
return response; return response;
} }
@ -118,9 +118,9 @@ export class UploadController {
throw new HttpException( throw new HttpException(
{ {
message: (error as Error).message || '生成分享码失败', message: (error as Error).message || '生成分享码失败',
error: 'SHARE_CODE_GENERATION_FAILED' error: 'SHARE_CODE_GENERATION_FAILED',
}, },
HttpStatus.INTERNAL_SERVER_ERROR HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@ -134,7 +134,7 @@ export class UploadController {
if (!data.fileId || !data.fileName) { if (!data.fileId || !data.fileName) {
throw new HttpException( throw new HttpException(
{ message: '缺少必要参数' }, { message: '缺少必要参数' },
HttpStatus.BAD_REQUEST HttpStatus.BAD_REQUEST,
); );
} }
// 保存文件名 // 保存文件名
@ -147,9 +147,9 @@ export class UploadController {
throw new HttpException( throw new HttpException(
{ {
message: '保存文件名失败', message: '保存文件名失败',
error: (error instanceof Error) ? error.message : String(error) error: error instanceof Error ? error.message : String(error),
}, },
HttpStatus.INTERNAL_SERVER_ERROR HttpStatus.INTERNAL_SERVER_ERROR,
); );
} }
} }
@ -167,19 +167,18 @@ export class UploadController {
} }
// 获取原始文件名 // 获取原始文件名
const fileName = await this.resourceService.getFileName(fileId) || 'downloaded-file'; const fileName =
(await this.resourceService.getFileName(fileId)) || 'downloaded-file';
// 设置响应头,包含原始文件名 // 设置响应头,包含原始文件名
res.setHeader( res.setHeader(
'Content-Disposition', 'Content-Disposition',
`attachment; filename="${encodeURIComponent(fileName)}"` `attachment; filename="${encodeURIComponent(fileName)}"`,
); );
// 其他下载逻辑... // 其他下载逻辑...
} catch (error) { } catch (error) {
// 错误处理... // 错误处理...
} }
} }
} }

View File

@ -3,6 +3,6 @@ import { MinioService } from './minio.service';
@Module({ @Module({
providers: [MinioService], providers: [MinioService],
exports: [MinioService] exports: [MinioService],
}) })
export class MinioModule {} export class MinioModule {}

View File

@ -1,13 +1,13 @@
import { redis } from "./redis.service"; import { redis } from './redis.service';
export async function deleteByPattern(pattern: string) { export async function deleteByPattern(pattern: string) {
try { try {
const keys = await redis.keys(pattern); const keys = await redis.keys(pattern);
if (keys.length > 0) { if (keys.length > 0) {
await redis.del(keys); await redis.del(keys);
// this.logger.log(`Deleted ${keys.length} keys matching pattern ${pattern}`); // this.logger.log(`Deleted ${keys.length} keys matching pattern ${pattern}`);
}
} catch (error) {
console.error(`Failed to delete keys by pattern ${pattern}:`, error);
} }
} catch (error) {
console.error(`Failed to delete keys by pattern ${pattern}:`, error);
}
} }

View File

@ -1,148 +1,149 @@
import { createReadStream } from "fs"; import { createReadStream } from 'fs';
import { createInterface } from "readline"; import { createInterface } from 'readline';
import { db } from '@nice/common'; import { db } from '@nice/common';
import * as tus from "tus-js-client"; import * as tus from 'tus-js-client';
import ExcelJS from 'exceljs'; import ExcelJS from 'exceljs';
export function truncateStringByByte(str, maxBytes) { export function truncateStringByByte(str, maxBytes) {
let byteCount = 0; let byteCount = 0;
let index = 0; let index = 0;
while (index < str.length && byteCount + new TextEncoder().encode(str[index]).length <= maxBytes) { while (
byteCount += new TextEncoder().encode(str[index]).length; index < str.length &&
index++; byteCount + new TextEncoder().encode(str[index]).length <= maxBytes
} ) {
return str.substring(0, index) + (index < str.length ? "..." : ""); byteCount += new TextEncoder().encode(str[index]).length;
index++;
}
return str.substring(0, index) + (index < str.length ? '...' : '');
} }
export async function loadPoliciesFromCSV(filePath: string) { export async function loadPoliciesFromCSV(filePath: string) {
const policies = { const policies = {
p: [], p: [],
g: [] g: [],
}; };
const stream = createReadStream(filePath); const stream = createReadStream(filePath);
const rl = createInterface({ const rl = createInterface({
input: stream, input: stream,
crlfDelay: Infinity crlfDelay: Infinity,
}); });
// Updated regex to handle commas inside parentheses as part of a single field // Updated regex to handle commas inside parentheses as part of a single field
const regex = /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g; const regex =
/(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g;
for await (const line of rl) { for await (const line of rl) {
// Ignore empty lines and comments // Ignore empty lines and comments
if (line.trim() && !line.startsWith("#")) { if (line.trim() && !line.startsWith('#')) {
const parts = []; const parts = [];
let match; let match;
while ((match = regex.exec(line)) !== null) { while ((match = regex.exec(line)) !== null) {
// Remove quotes if present and trim whitespace // Remove quotes if present and trim whitespace
parts.push(match[0].replace(/^"|"$/g, '').trim()); parts.push(match[0].replace(/^"|"$/g, '').trim());
} }
// Check policy type (p or g) // Check policy type (p or g)
const ptype = parts[0]; const ptype = parts[0];
const rule = parts.slice(1); const rule = parts.slice(1);
if (ptype === 'p' || ptype === 'g') { if (ptype === 'p' || ptype === 'g') {
policies[ptype].push(rule); policies[ptype].push(rule);
} else { } else {
console.warn(`Unknown policy type '${ptype}' in policy: ${line}`); console.warn(`Unknown policy type '${ptype}' in policy: ${line}`);
} }
}
} }
}
return policies; return policies;
} }
export function uploadFile(blob: any, fileName: string) { export function uploadFile(blob: any, fileName: string) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const upload = new tus.Upload(blob, { const upload = new tus.Upload(blob, {
endpoint: `${process.env.TUS_URL}/files/`, endpoint: `${process.env.TUS_URL}/files/`,
retryDelays: [0, 1000, 3000, 5000], retryDelays: [0, 1000, 3000, 5000],
metadata: { metadata: {
filename: fileName, filename: fileName,
filetype: filetype:
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
}, },
onError: (error) => { onError: (error) => {
console.error("Failed because: " + error); console.error('Failed because: ' + error);
reject(error); // 错误时,我们要拒绝 promise reject(error); // 错误时,我们要拒绝 promise
}, },
onProgress: (bytesUploaded, bytesTotal) => { onProgress: (bytesUploaded, bytesTotal) => {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
// console.log(bytesUploaded, bytesTotal, `${percentage}%`); // console.log(bytesUploaded, bytesTotal, `${percentage}%`);
}, },
onSuccess: () => { onSuccess: () => {
// console.log('Upload finished:', upload.url); // console.log('Upload finished:', upload.url);
resolve(upload.url); // 成功后,我们解析 promise并返回上传的 URL resolve(upload.url); // 成功后,我们解析 promise并返回上传的 URL
}, },
});
upload.start();
}); });
upload.start();
});
} }
class TreeNode { class TreeNode {
value: string; value: string;
children: TreeNode[]; 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)
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 { function buildTree(data: string[][]): TreeNode {
const root = new TreeNode('root'); const root = new TreeNode('root');
try { try {
for (const path of data) { for (const path of data) {
let currentNode = root; let currentNode = root;
for (const value of path) { for (const value of path) {
currentNode = currentNode.addChild(value); currentNode = currentNode.addChild(value);
} }
}
return root;
} }
catch (error) { return root;
console.error(error) } catch (error) {
} console.error(error);
}
} }
export function printTree(node: TreeNode, level: number = 0): void { export function printTree(node: TreeNode, level: number = 0): void {
const indent = ' '.repeat(level); const indent = ' '.repeat(level);
// console.log(`${indent}${node.value}`); // console.log(`${indent}${node.value}`);
for (const child of node.children) { for (const child of node.children) {
printTree(child, level + 1); printTree(child, level + 1);
} }
} }
export async function generateTreeFromFile(file: Buffer): Promise<TreeNode> { export async function generateTreeFromFile(file: Buffer): Promise<TreeNode> {
const workbook = new ExcelJS.Workbook(); const workbook = new ExcelJS.Workbook();
await workbook.xlsx.load(file); await workbook.xlsx.load(file);
const worksheet = workbook.getWorksheet(1); const worksheet = workbook.getWorksheet(1);
const data: string[][] = []; const data: string[][] = [];
worksheet.eachRow((row, rowNumber) => { worksheet.eachRow((row, rowNumber) => {
if (rowNumber > 1) { // Skip header row if any if (rowNumber > 1) {
const rowData: string[] = (row.values as string[]).slice(2).map(cell => (cell || '').toString()); // Skip header row if any
data.push(rowData.map(value => value.trim())); const rowData: string[] = (row.values as string[])
} .slice(2)
}); .map((cell) => (cell || '').toString());
// Fill forward values data.push(rowData.map((value) => value.trim()));
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); });
// 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);
} }

View File

@ -2,17 +2,17 @@ import { PipeTransform, BadRequestException } from '@nestjs/common';
import { ZodSchema } from 'zod'; import { ZodSchema } from 'zod';
export class ZodValidationPipe implements PipeTransform { export class ZodValidationPipe implements PipeTransform {
constructor(private schema: ZodSchema) { } constructor(private schema: ZodSchema) {}
transform(value: unknown) { transform(value: unknown) {
try { try {
const result = this.schema.parse(value); const result = this.schema.parse(value);
return result; return result;
} catch (error: any) { } catch (error: any) {
throw new BadRequestException('Validation failed', { throw new BadRequestException('Validation failed', {
cause: error, cause: error,
description: error.errors description: error.errors,
}); });
}
} }
}
} }

View File

@ -0,0 +1,96 @@
import { api } from '@nice/client';
import { Pagination } from 'antd';
import React, { useState } from 'react';
// 定义 TrainSituation 接口
interface TrainSituation {
id: string;
staffId: string;
score: number;
}
// 定义 Staff 接口
interface Staff {
id: string;
username: string;
absent: boolean;
// trainSituations: TrainSituation[];
}
interface PaginatedResponse {
items: Staff[];
total: number;
}
const TestPage: React.FC = () => {
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 10;
// 使用 findManyWithPagination 替换原来的两个查询
const { data, isLoading } = api.staff.findManyWithPagination.useQuery<PaginatedResponse>({
page: currentPage,
pageSize: pageSize,
where: { deletedAt: null },
select: {
id: true,
username: true,
absent: true,
}
});
console.log(data);
// data 中包含了分页数据和总记录数
const staffs = (data?.items || []) as Staff[];
const totalCount = data?.total || 0;
// 分页处理函数
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4 text-center"></h1>
<div className="overflow-x-auto">
<table className="min-w-full bg-white shadow-md rounded-lg">
<thead>
<tr className="bg-gray-100 border-b">
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">ID</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider">ID</th>
<th className="px-6 py-3 text-center text-xs font-medium text-gray-600 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{staffs.map((staff) => (
<tr key={staff.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-center">
{staff.id}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
{staff.username}
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
{staff.absent ? '在位' : '不在位'}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 flex justify-center">
<Pagination
current={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
showTotal={(total) => `${total} 条记录`}
showSizeChanger={false}
showQuickJumper
/>
</div>
</div>
);
};
export default TestPage;

0
apps/web/src/app/main/home/ChartControls.tsx Normal file → Executable file
View File

0
apps/web/src/app/main/home/DepartmentCharts.tsx Normal file → Executable file
View File

110
apps/web/src/app/main/home/DepartmentTable.tsx Normal file → Executable file
View File

@ -1,23 +1,127 @@
import React from 'react'; import React from "react";
import { Row, Col, Table, Badge, Empty, Tooltip } from "antd"; import { Row, Col, Table, Badge, Empty, Tooltip } from "antd";
import { TeamOutlined, InfoCircleOutlined } from "@ant-design/icons"; import { TeamOutlined, InfoCircleOutlined } from "@ant-design/icons";
import DashboardCard from "../../../components/presentation/dashboard-card"; import DashboardCard from "../../../components/presentation/dashboard-card";
import { theme } from "antd"; import { theme } from "antd";
import { EChartsOption } from "echarts-for-react";
interface DeptData { interface DeptData {
id: string; id: string;
name: string; name: string;
count: number; count: number;
} }
interface DepartmentTableProps { interface DepartmentTableProps {
filteredDeptData: DeptData[]; filteredDeptData: DeptData[];
staffs: any[] | undefined; staffs: any[] | undefined;
} }
// 图表配置函数
export const getPieChartOptions = (
deptData: DeptData[],
onEvents?: Record<string, (params: any) => void>
): EChartsOption => {
const top10Depts = deptData.slice(0, 10);
return {
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b}: {c}人 ({d}%)",
},
legend: {
orient: "vertical",
right: 10,
top: "center",
data: top10Depts.map((dept) => dept.name),
},
series: [
{
name: "部门人数",
type: "pie",
radius: ["50%", "70%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: "#fff",
borderWidth: 2,
},
label: {
show: true,
formatter: "{b}: {c}人 ({d}%)",
position: "outside",
},
emphasis: {
label: {
show: true,
fontSize: "18",
fontWeight: "bold",
},
},
labelLine: {
show: true,
},
data: top10Depts.map((dept) => ({
value: dept.count,
name: dept.name,
})),
},
],
};
};
export const getBarChartOptions = (deptData: DeptData[]): EChartsOption => {
const top10Depts = deptData.slice(0, 10);
return {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
grid: {
left: "3%",
right: "12%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "value",
boundaryGap: [0, 0.01],
},
yAxis: {
type: "category",
data: top10Depts.map((dept) => dept.name),
inverse: true,
},
series: [
{
name: "人员数量",
type: "bar",
data: top10Depts.map((dept) => dept.count),
itemStyle: {
color: function (params) {
const colorList = [
"#91cc75",
"#5470c6",
"#ee6666",
"#73c0de",
"#3ba272",
"#fc8452",
"#9a60b4",
];
return colorList[params.dataIndex % colorList.length];
},
},
label: {
show: true,
position: "right",
formatter: "{c}人",
},
},
],
};
};
export default function DepartmentTable({ export default function DepartmentTable({
filteredDeptData, filteredDeptData,
staffs staffs,
}: DepartmentTableProps): React.ReactElement { }: DepartmentTableProps): React.ReactElement {
const { token } = theme.useToken(); const { token } = theme.useToken();

13
apps/web/src/app/main/home/StatisticCards.tsx Normal file → Executable file
View File

@ -1,13 +1,18 @@
import React from 'react'; import React from "react";
import { Row, Col, Statistic, Tooltip } from "antd"; import { Row, Col, Statistic, Tooltip } from "antd";
import { UserOutlined, TeamOutlined, IdcardOutlined, InfoCircleOutlined } from "@ant-design/icons"; import {
UserOutlined,
TeamOutlined,
IdcardOutlined,
InfoCircleOutlined,
} from "@ant-design/icons";
import DashboardCard from "../../../components/presentation/dashboard-card"; import DashboardCard from "../../../components/presentation/dashboard-card";
import { theme } from "antd"; import { theme } from "antd";
interface PositionStats { interface PositionStats {
total: number; total: number;
distribution: any[]; distribution: any[];
topPosition: {name: string; count: number} | null; topPosition: { name: string; count: number } | null;
vacantPositions: number; vacantPositions: number;
} }
@ -20,7 +25,7 @@ interface StatisticCardsProps {
export default function StatisticCards({ export default function StatisticCards({
staffs, staffs,
departments, departments,
positionStats positionStats,
}: StatisticCardsProps): React.ReactElement { }: StatisticCardsProps): React.ReactElement {
const { token } = theme.useToken(); const { token } = theme.useToken();

View File

@ -1,111 +0,0 @@
import { EChartsOption } from "echarts-for-react";
interface DeptData {
name: string;
count: number;
id: string;
}
// 图表配置函数
export const getPieChartOptions = (
deptData: DeptData[],
onEvents?: Record<string, (params: any) => void>
): EChartsOption => {
const top10Depts = deptData.slice(0, 10);
return {
tooltip: {
trigger: "item",
formatter: "{a} <br/>{b}: {c}人 ({d}%)",
},
legend: {
orient: "vertical",
right: 10,
top: "center",
data: top10Depts.map((dept) => dept.name),
},
series: [
{
name: "部门人数",
type: "pie",
radius: ["50%", "70%"],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: "#fff",
borderWidth: 2,
},
label: {
show: true,
formatter: "{b}: {c}人 ({d}%)",
position: "outside",
},
emphasis: {
label: {
show: true,
fontSize: "18",
fontWeight: "bold",
},
},
labelLine: {
show: true,
},
data: top10Depts.map((dept) => ({
value: dept.count,
name: dept.name,
})),
},
],
};
};
export const getBarChartOptions = (deptData: DeptData[]): EChartsOption => {
const top10Depts = deptData.slice(0, 10);
return {
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
grid: {
left: "3%",
right: "12%",
bottom: "3%",
containLabel: true,
},
xAxis: {
type: "value",
boundaryGap: [0, 0.01],
},
yAxis: {
type: "category",
data: top10Depts.map((dept) => dept.name),
inverse: true,
},
series: [
{
name: "人员数量",
type: "bar",
data: top10Depts.map((dept) => dept.count),
itemStyle: {
color: function (params) {
const colorList = [
"#91cc75",
"#5470c6",
"#ee6666",
"#73c0de",
"#3ba272",
"#fc8452",
"#9a60b4",
];
return colorList[params.dataIndex % colorList.length];
},
},
label: {
show: true,
position: "right",
formatter: "{c}人",
},
},
],
};
};

View File

@ -6,7 +6,7 @@ import StatisticCards from "./StatisticCards";
import ChartControls from "./ChartControls"; import ChartControls from "./ChartControls";
import DepartmentCharts from "./DepartmentCharts"; import DepartmentCharts from "./DepartmentCharts";
import DepartmentTable from "./DepartmentTable"; import DepartmentTable from "./DepartmentTable";
import { getPieChartOptions, getBarChartOptions } from "./char-options"; import { getPieChartOptions, getBarChartOptions } from "./DepartmentTable";
interface DeptData { interface DeptData {
id: string; id: string;
name: string; name: string;

View File

@ -51,6 +51,13 @@ const items = [
null, null,
null, null,
), ),
getItem(
"test",
"/test",
<i className="iconfont icon-icon-category" />,
null,
null,
),
getItem( getItem(
"系统设置", "系统设置",
"/admin", "/admin",

View File

@ -352,8 +352,9 @@ export default function StaffMessage() {
const createMany = api.staff.create.useMutation({ const createMany = api.staff.create.useMutation({
onSuccess: () => { onSuccess: () => {
message.success("员工数据导入成功"); message.success("员工数据导入成功");
// 刷新数据 // 不要在这里直接调用 useQuery 钩子
api.staff.findMany.useQuery(); // 而是使用 trpc 的 invalidate 方法来刷新数据
api.useContext().staff.findMany.invalidate();
setImportVisible(false); setImportVisible(false);
}, },
onError: (error) => { onError: (error) => {
@ -404,25 +405,33 @@ export default function StaffMessage() {
} }
} }
// 我们不在这里处理自定义字段,而是在员工创建后单独处理
console.log(`准备创建员工: ${staff.showname}`); console.log(`准备创建员工: ${staff.showname}`);
return staff; return staff;
}); });
// 逐条导入数据 // 逐条导入数据
if (staffImportData.length > 0) { if (staffImportData.length > 0) {
let importedCount = 0;
const totalCount = staffImportData.length;
staffImportData.forEach((staffData, index) => { staffImportData.forEach((staffData, index) => {
createMany.mutate( createMany.mutate(
{ data: staffData }, { data: staffData },
{ {
onSuccess: (data) => { onSuccess: (data) => {
console.log(`员工创建成功:`, data); console.log(`员工创建成功:`, data);
message.success(`成功导入第${index + 1}条基础数据`); importedCount++;
// 员工创建成功后,再单独处理自定义字段值 // 显示进度信息
// 由于外键约束问题,我们暂时跳过字段值的创建 if (importedCount === totalCount) {
// 后续可以添加专门的字段值导入功能 message.success(`所有${totalCount}条数据导入完成`);
// 在所有数据导入完成后刷新查询
api.useContext().staff.findMany.invalidate();
} else {
message.info(
`已导入 ${importedCount}/${totalCount} 条数据`
);
}
}, },
onError: (error) => { onError: (error) => {
message.error( message.error(
@ -434,7 +443,7 @@ export default function StaffMessage() {
); );
}); });
message.info(`正在导入${staffImportData.length}条员工数据...`); message.info(`开始导入${staffImportData.length}条员工数据...`);
} }
} catch (error) { } catch (error) {
console.error("处理导入数据失败:", error); console.error("处理导入数据失败:", error);
@ -559,12 +568,10 @@ export default function StaffMessage() {
try { try {
const wb = read(e.target?.result); const wb = read(e.target?.result);
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
if (data.length === 0) { if (data.length === 0) {
message.warning("Excel文件中没有数据"); message.warning("Excel文件中没有数据");
return; return;
} }
message.info(`读取到${data.length}条数据,正在处理...`); message.info(`读取到${data.length}条数据,正在处理...`);
handleImportData(data); handleImportData(data);
} catch (error) { } catch (error) {

View File

@ -7,17 +7,20 @@ import {
} from "react-router-dom"; } from "react-router-dom";
import ErrorPage from "../app/error"; import ErrorPage from "../app/error";
import LoginPage from "../app/login"; import LoginPage from "../app/login";
import HomePage from "../app/main/home/page"; // import HomePage from "../app/main/home/page";
import StaffMessage from "../app/main/staffinfo_show/staffmessage_page"; import StaffMessage from "../app/main/staffinfo_show/staffmessage_page";
import MainLayout from "../app/main/layout/MainLayout"; import MainLayout from "../app/main/layout/MainLayout";
import DailyPage from "../app/main/daily/page"; // import DailyPage from "../app/main/daily/page";
import Dashboard from "../app/main/home/page"; import Dashboard from "../app/main/home/page";
import WeekPlanPage from "../app/main/plan/weekplan/page"; // import WeekPlanPage from "../app/main/plan/weekplan/page";
import StaffInformation from "../app/main/staffinfo_write/staffinfo_write.page"; import StaffInformation from "../app/main/staffinfo_write/staffinfo_write.page";
import DeptSettingPage from "../app/main/admin/deptsettingpage/page"; import DeptSettingPage from "../app/main/admin/deptsettingpage/page";
import { adminRoute } from "./admin-route"; import { adminRoute } from "./admin-route";
import AdminLayout from "../components/layout/admin/AdminLayout"; import AdminLayout from "../components/layout/admin/AdminLayout";
import SystemLogPage from "../app/main/systemlog/SystemLogPage"; import SystemLogPage from "../app/main/systemlog/SystemLogPage";
import TestPage from "../app/main/Test/Page";
interface CustomIndexRouteObject extends IndexRouteObject { interface CustomIndexRouteObject extends IndexRouteObject {
name?: string; name?: string;
breadcrumb?: string; breadcrumb?: string;
@ -71,6 +74,10 @@ export const routes: CustomRouteObject[] = [
element: <AdminLayout></AdminLayout>, element: <AdminLayout></AdminLayout>,
children: adminRoute.children, children: adminRoute.children,
}, },
{
path: "/test",
element: <TestPage></TestPage>,
},
], ],
}, },
], ],

View File

@ -2,7 +2,7 @@ server {
# 监听80端口 # 监听80端口
listen 80; listen 80;
# 服务器域名/IP地址使用环境变量 # 服务器域名/IP地址使用环境变量
server_name 192.168.252.77; server_name 192.168.217.194;
# 基础性能优化配置 # 基础性能优化配置
# 启用tcp_nopush以优化数据发送 # 启用tcp_nopush以优化数据发送
@ -100,7 +100,7 @@ server {
# 仅供内部使用 # 仅供内部使用
internal; internal;
# 代理到认证服务 # 代理到认证服务
proxy_pass http://192.168.252.77:3001/auth/file; proxy_pass http://192.168.217.194:3001/auth/file;
# 请求优化:不传递请求体 # 请求优化:不传递请求体
proxy_pass_request_body off; proxy_pass_request_body off;

0
package-lock.json generated Normal file → Executable file
View File