Merge branch 'main' of http://113.45.67.59:3003/linfeng/training_data
This commit is contained in:
commit
758678b729
|
@ -1,3 +1,6 @@
|
|||
{
|
||||
"marscode.chatLanguage": "cn"
|
||||
"marscode.chatLanguage": "cn",
|
||||
"marscode.codeCompletionPro": {
|
||||
"enableCodeCompletionPro": true
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,12 +23,12 @@ import { UploadModule } from './upload/upload.module';
|
|||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true, // 全局可用
|
||||
envFilePath: '.env'
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
secret: env.JWT_SECRET
|
||||
secret: env.JWT_SECRET,
|
||||
}),
|
||||
WebSocketModule,
|
||||
TrpcModule,
|
||||
|
@ -42,11 +42,13 @@ import { UploadModule } from './upload/upload.module';
|
|||
MinioModule,
|
||||
CollaborationModule,
|
||||
RealTimeModule,
|
||||
UploadModule
|
||||
UploadModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: ExceptionsFilter,
|
||||
},
|
||||
],
|
||||
providers: [{
|
||||
provide: APP_FILTER,
|
||||
useClass: ExceptionsFilter,
|
||||
}],
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
UnauthorizedException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthSchema, JwtPayload } from '@nice/common';
|
||||
import { AuthGuard } from './auth.guard';
|
||||
|
@ -43,8 +44,9 @@ export class AuthController {
|
|||
authorization,
|
||||
};
|
||||
|
||||
const authResult =
|
||||
await this.authService.validateFileRequest(fileRequest);
|
||||
const authResult = await this.authService.validateFileRequest(
|
||||
fileRequest,
|
||||
);
|
||||
if (!authResult.isValid) {
|
||||
// 使用枚举类型进行错误处理
|
||||
switch (authResult.error) {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { env } from '@server/env';
|
||||
|
@ -12,26 +12,21 @@ import { extractTokenFromHeader } from './utils';
|
|||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private jwtService: JwtService) { }
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload: JwtPayload = await this.jwtService.verifyAsync(
|
||||
token,
|
||||
{
|
||||
secret: env.JWT_SECRET
|
||||
}
|
||||
);
|
||||
request['user'] = payload;
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return true;
|
||||
constructor(private jwtService: JwtService) {}
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = extractTokenFromHeader(request);
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
try {
|
||||
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
|
||||
secret: env.JWT_SECRET,
|
||||
});
|
||||
request['user'] = payload;
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,8 @@ import { SessionService } from './session.service';
|
|||
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
||||
@Module({
|
||||
imports: [StaffModule, RoleMapModule],
|
||||
providers: [
|
||||
AuthService,
|
||||
TrpcService,
|
||||
DepartmentService,
|
||||
SessionService],
|
||||
providers: [AuthService, TrpcService, DepartmentService, SessionService],
|
||||
exports: [AuthService],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule { }
|
||||
export class AuthModule {}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
export const tokenConfig = {
|
||||
accessToken: {
|
||||
expirationMs: 7 * 24 * 3600000, // 7 days
|
||||
expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds
|
||||
},
|
||||
refreshToken: {
|
||||
expirationMs: 30 * 24 * 3600000, // 30 days
|
||||
},
|
||||
};
|
||||
accessToken: {
|
||||
expirationMs: 7 * 24 * 3600000, // 7 days
|
||||
expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds
|
||||
},
|
||||
refreshToken: {
|
||||
expirationMs: 30 * 24 * 3600000, // 30 days
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,58 +4,63 @@ import { redis } from '@server/utils/redis/redis.service';
|
|||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export interface SessionInfo {
|
||||
session_id: string;
|
||||
access_token: string;
|
||||
access_token_expires_at: number;
|
||||
refresh_token: string;
|
||||
refresh_token_expires_at: number;
|
||||
session_id: string;
|
||||
access_token: string;
|
||||
access_token_expires_at: number;
|
||||
refresh_token: string;
|
||||
refresh_token_expires_at: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SessionService {
|
||||
private getSessionKey(userId: string, sessionId: string): string {
|
||||
return `session-${userId}-${sessionId}`;
|
||||
}
|
||||
async createSession(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expirationConfig: {
|
||||
accessTokenExpirationMs: number;
|
||||
refreshTokenExpirationMs: number;
|
||||
sessionTTL: number;
|
||||
},
|
||||
): Promise<SessionInfo> {
|
||||
const sessionInfo: SessionInfo = {
|
||||
session_id: uuidv4(),
|
||||
access_token: accessToken,
|
||||
access_token_expires_at: Date.now() + expirationConfig.accessTokenExpirationMs,
|
||||
refresh_token: refreshToken,
|
||||
refresh_token_expires_at: Date.now() + expirationConfig.refreshTokenExpirationMs,
|
||||
};
|
||||
private getSessionKey(userId: string, sessionId: string): string {
|
||||
return `session-${userId}-${sessionId}`;
|
||||
}
|
||||
async createSession(
|
||||
userId: string,
|
||||
accessToken: string,
|
||||
refreshToken: string,
|
||||
expirationConfig: {
|
||||
accessTokenExpirationMs: number;
|
||||
refreshTokenExpirationMs: number;
|
||||
sessionTTL: number;
|
||||
},
|
||||
): Promise<SessionInfo> {
|
||||
const sessionInfo: SessionInfo = {
|
||||
session_id: uuidv4(),
|
||||
access_token: accessToken,
|
||||
access_token_expires_at:
|
||||
Date.now() + expirationConfig.accessTokenExpirationMs,
|
||||
refresh_token: refreshToken,
|
||||
refresh_token_expires_at:
|
||||
Date.now() + expirationConfig.refreshTokenExpirationMs,
|
||||
};
|
||||
|
||||
await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL);
|
||||
return sessionInfo;
|
||||
}
|
||||
await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL);
|
||||
return sessionInfo;
|
||||
}
|
||||
|
||||
async getSession(userId: string, sessionId: string): Promise<SessionInfo | null> {
|
||||
const sessionData = await redis.get(this.getSessionKey(userId, sessionId));
|
||||
return sessionData ? JSON.parse(sessionData) : null;
|
||||
}
|
||||
async getSession(
|
||||
userId: string,
|
||||
sessionId: string,
|
||||
): Promise<SessionInfo | null> {
|
||||
const sessionData = await redis.get(this.getSessionKey(userId, sessionId));
|
||||
return sessionData ? JSON.parse(sessionData) : null;
|
||||
}
|
||||
|
||||
async saveSession(
|
||||
userId: string,
|
||||
sessionInfo: SessionInfo,
|
||||
ttl: number,
|
||||
): Promise<void> {
|
||||
await redis.setex(
|
||||
this.getSessionKey(userId, sessionInfo.session_id),
|
||||
ttl,
|
||||
JSON.stringify(sessionInfo),
|
||||
);
|
||||
}
|
||||
async saveSession(
|
||||
userId: string,
|
||||
sessionInfo: SessionInfo,
|
||||
ttl: number,
|
||||
): Promise<void> {
|
||||
await redis.setex(
|
||||
this.getSessionKey(userId, sessionInfo.session_id),
|
||||
ttl,
|
||||
JSON.stringify(sessionInfo),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteSession(userId: string, sessionId: string): Promise<void> {
|
||||
await redis.del(this.getSessionKey(userId, sessionId));
|
||||
}
|
||||
}
|
||||
async deleteSession(userId: string, sessionId: string): Promise<void> {
|
||||
await redis.del(this.getSessionKey(userId, sessionId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
export interface TokenConfig {
|
||||
accessToken: {
|
||||
expirationMs: number;
|
||||
expirationTTL: number;
|
||||
};
|
||||
refreshToken: {
|
||||
expirationMs: number;
|
||||
};
|
||||
accessToken: {
|
||||
expirationMs: number;
|
||||
expirationTTL: number;
|
||||
};
|
||||
refreshToken: {
|
||||
expirationMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FileAuthResult {
|
||||
isValid: boolean
|
||||
userId?: string
|
||||
resourceType?: string
|
||||
error?: string
|
||||
isValid: boolean;
|
||||
userId?: string;
|
||||
resourceType?: string;
|
||||
error?: string;
|
||||
}
|
||||
export interface FileRequest {
|
||||
originalUri: string;
|
||||
realIp: string;
|
||||
method: string;
|
||||
queryParams: string;
|
||||
host: string;
|
||||
authorization: string
|
||||
originalUri: string;
|
||||
realIp: string;
|
||||
method: string;
|
||||
queryParams: string;
|
||||
host: string;
|
||||
authorization: string;
|
||||
}
|
||||
export enum FileValidationErrorType {
|
||||
INVALID_URI = 'INVALID_URI',
|
||||
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
||||
AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED',
|
||||
INVALID_TOKEN = 'INVALID_TOKEN',
|
||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
|
||||
}
|
||||
INVALID_URI = 'INVALID_URI',
|
||||
RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND',
|
||||
AUTHORIZATION_REQUIRED = 'AUTHORIZATION_REQUIRED',
|
||||
INVALID_TOKEN = 'INVALID_TOKEN',
|
||||
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { env } from '@server/env';
|
|||
import { redis } from '@server/utils/redis/redis.service';
|
||||
import EventBus from '@server/utils/event-bus';
|
||||
import { RoleMapService } from '@server/models/rbac/rolemap.service';
|
||||
import { Request } from "express"
|
||||
import { Request } from 'express';
|
||||
interface ProfileResult {
|
||||
staff: UserProfile | undefined;
|
||||
error?: string;
|
||||
|
@ -22,9 +22,11 @@ interface TokenVerifyResult {
|
|||
error?: string;
|
||||
}
|
||||
export function extractTokenFromHeader(request: Request): string | undefined {
|
||||
return extractTokenFromAuthorization(request.headers.authorization)
|
||||
return extractTokenFromAuthorization(request.headers.authorization);
|
||||
}
|
||||
export function extractTokenFromAuthorization(authorization: string): string | undefined {
|
||||
export function extractTokenFromAuthorization(
|
||||
authorization: string,
|
||||
): string | undefined {
|
||||
const [type, token] = authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
|
@ -40,7 +42,7 @@ export class UserProfileService {
|
|||
this.jwtService = new JwtService();
|
||||
this.departmentService = new DepartmentService();
|
||||
this.roleMapService = new RoleMapService(this.departmentService);
|
||||
EventBus.on("dataChanged", ({ type, data }) => {
|
||||
EventBus.on('dataChanged', ({ type, data }) => {
|
||||
if (type === ObjectType.STAFF) {
|
||||
// 确保 data 是数组,如果不是则转换为数组
|
||||
const dataArray = Array.isArray(data) ? data : [data];
|
||||
|
@ -51,7 +53,6 @@ export class UserProfileService {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
public getProfileCacheKey(id: string) {
|
||||
return `user-profile-${id}`;
|
||||
|
@ -175,9 +176,7 @@ export class UserProfileService {
|
|||
staff.deptId
|
||||
? this.departmentService.getDescendantIdsInDomain(staff.deptId)
|
||||
: [],
|
||||
staff.deptId
|
||||
? this.departmentService.getAncestorIds([staff.deptId])
|
||||
: [],
|
||||
staff.deptId ? this.departmentService.getAncestorIds([staff.deptId]) : [],
|
||||
this.roleMapService.getPermsForObject({
|
||||
domainId: staff.domainId,
|
||||
staffId: staff.id,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export const env: { JWT_SECRET: string } = {
|
||||
JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA='
|
||||
}
|
||||
JWT_SECRET:
|
||||
process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA=',
|
||||
};
|
||||
|
|
|
@ -7,6 +7,6 @@ import { RealTimeModule } from '@server/socket/realtime/realtime.module';
|
|||
@Module({
|
||||
imports: [RealTimeModule],
|
||||
providers: [AppConfigService, AppConfigRouter, TrpcService],
|
||||
exports: [AppConfigService, AppConfigRouter]
|
||||
exports: [AppConfigService, AppConfigRouter],
|
||||
})
|
||||
export class AppConfigModule { }
|
||||
export class AppConfigModule {}
|
||||
|
|
|
@ -30,6 +30,7 @@ export class BaseService<
|
|||
A extends DelegateArgs<D> = DelegateArgs<D>,
|
||||
R extends DelegateReturnTypes<D> = DelegateReturnTypes<D>,
|
||||
> {
|
||||
[x: string]: any;
|
||||
protected ORDER_INTERVAL = 100;
|
||||
/**
|
||||
* Initializes the BaseService with the specified model.
|
||||
|
@ -152,27 +153,27 @@ export class BaseService<
|
|||
* @example
|
||||
* const newUser = await service.create({ data: { name: 'John Doe' } });
|
||||
*/
|
||||
async create(args: A['create'], params?: any): Promise<R['create']> {
|
||||
try {
|
||||
if (this.enableOrder && !(args as any).data.order) {
|
||||
// 查找当前最大的 order 值
|
||||
const maxOrderItem = (await this.getModel(params?.tx).findFirst({
|
||||
orderBy: { order: 'desc' },
|
||||
})) as any;
|
||||
// 设置新记录的 order 值
|
||||
const newOrder = maxOrderItem
|
||||
? maxOrderItem.order + this.ORDER_INTERVAL
|
||||
: 1;
|
||||
// 将 order 添加到创建参数中
|
||||
(args as any).data.order = newOrder;
|
||||
}
|
||||
return this.getModel(params?.tx).create(args as any) as Promise<
|
||||
R['create']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'create');
|
||||
}
|
||||
}
|
||||
// async create(args: A['create'], params?: any): Promise<R['create']> {
|
||||
// try {
|
||||
// if (this.enableOrder && !(args as any).data.order) {
|
||||
// // 查找当前最大的 order 值
|
||||
// const maxOrderItem = (await this.getModel(params?.tx).findFirst({
|
||||
// orderBy: { order: 'desc' },
|
||||
// })) as any;
|
||||
// // 设置新记录的 order 值
|
||||
// const newOrder = maxOrderItem
|
||||
// ? maxOrderItem.order + this.ORDER_INTERVAL
|
||||
// : 1;
|
||||
// // 将 order 添加到创建参数中
|
||||
// (args as any).data.order = newOrder;
|
||||
// }
|
||||
// return this.getModel(params?.tx).create(args as any) as Promise<
|
||||
// R['create']
|
||||
// >;
|
||||
// } catch (error) {
|
||||
// this.handleError(error, 'create');
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Creates multiple new records with the given data.
|
||||
|
|
|
@ -1,198 +1,236 @@
|
|||
import {
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
export const PrismaErrorCode = Object.freeze({
|
||||
P2000: 'P2000',
|
||||
P2001: 'P2001',
|
||||
P2002: 'P2002',
|
||||
P2003: 'P2003',
|
||||
P2006: 'P2006',
|
||||
P2007: 'P2007',
|
||||
P2008: 'P2008',
|
||||
P2009: 'P2009',
|
||||
P2010: 'P2010',
|
||||
P2011: 'P2011',
|
||||
P2012: 'P2012',
|
||||
P2014: 'P2014',
|
||||
P2015: 'P2015',
|
||||
P2016: 'P2016',
|
||||
P2017: 'P2017',
|
||||
P2018: 'P2018',
|
||||
P2019: 'P2019',
|
||||
P2021: 'P2021',
|
||||
P2023: 'P2023',
|
||||
P2025: 'P2025',
|
||||
P2031: 'P2031',
|
||||
P2033: 'P2033',
|
||||
P2034: 'P2034',
|
||||
P2037: 'P2037',
|
||||
P1000: 'P1000',
|
||||
P1001: 'P1001',
|
||||
P1002: 'P1002',
|
||||
P1015: 'P1015',
|
||||
P1017: 'P1017',
|
||||
});
|
||||
|
||||
export type PrismaErrorCode = keyof typeof PrismaErrorCode;
|
||||
|
||||
|
||||
interface PrismaErrorMeta {
|
||||
target?: string;
|
||||
model?: string;
|
||||
relationName?: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
export type operationT = 'create' | 'read' | 'update' | 'delete';
|
||||
|
||||
export type PrismaErrorHandler = (
|
||||
operation: operationT,
|
||||
meta?: PrismaErrorMeta,
|
||||
) => Error;
|
||||
|
||||
export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = {
|
||||
P2000: (_operation, meta) => new BadRequestException(
|
||||
`The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.`
|
||||
),
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
ConflictException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
|
||||
P2001: (operation, meta) => new NotFoundException(
|
||||
`The ${meta?.model || 'record'} you are trying to ${operation} could not be found.`
|
||||
),
|
||||
export const PrismaErrorCode = Object.freeze({
|
||||
P2000: 'P2000',
|
||||
P2001: 'P2001',
|
||||
P2002: 'P2002',
|
||||
P2003: 'P2003',
|
||||
P2006: 'P2006',
|
||||
P2007: 'P2007',
|
||||
P2008: 'P2008',
|
||||
P2009: 'P2009',
|
||||
P2010: 'P2010',
|
||||
P2011: 'P2011',
|
||||
P2012: 'P2012',
|
||||
P2014: 'P2014',
|
||||
P2015: 'P2015',
|
||||
P2016: 'P2016',
|
||||
P2017: 'P2017',
|
||||
P2018: 'P2018',
|
||||
P2019: 'P2019',
|
||||
P2021: 'P2021',
|
||||
P2023: 'P2023',
|
||||
P2025: 'P2025',
|
||||
P2031: 'P2031',
|
||||
P2033: 'P2033',
|
||||
P2034: 'P2034',
|
||||
P2037: 'P2037',
|
||||
P1000: 'P1000',
|
||||
P1001: 'P1001',
|
||||
P1002: 'P1002',
|
||||
P1015: 'P1015',
|
||||
P1017: 'P1017',
|
||||
});
|
||||
|
||||
P2002: (operation, meta) => {
|
||||
const field = meta?.target || 'unique field';
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
return new ConflictException(
|
||||
`A record with the same ${field} already exists. Please use a different value.`
|
||||
);
|
||||
case 'update':
|
||||
return new ConflictException(
|
||||
`The new value for ${field} conflicts with an existing record.`
|
||||
);
|
||||
default:
|
||||
return new ConflictException(
|
||||
`Unique constraint violation on ${field}.`
|
||||
);
|
||||
}
|
||||
},
|
||||
export type PrismaErrorCode = keyof typeof PrismaErrorCode;
|
||||
|
||||
P2003: (operation) => new BadRequestException(
|
||||
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`
|
||||
),
|
||||
interface PrismaErrorMeta {
|
||||
target?: string;
|
||||
model?: string;
|
||||
relationName?: string;
|
||||
details?: string;
|
||||
}
|
||||
|
||||
P2006: (_operation, meta) => new BadRequestException(
|
||||
`The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.`
|
||||
),
|
||||
export type operationT = 'create' | 'read' | 'update' | 'delete';
|
||||
|
||||
P2007: (operation) => new InternalServerErrorException(
|
||||
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.`
|
||||
),
|
||||
export type PrismaErrorHandler = (
|
||||
operation: operationT,
|
||||
meta?: PrismaErrorMeta,
|
||||
) => Error;
|
||||
|
||||
P2008: (operation) => new InternalServerErrorException(
|
||||
`Failed to query the database during ${operation}. Please try again later.`
|
||||
),
|
||||
export const ERROR_MAP: Record<PrismaErrorCode, PrismaErrorHandler> = {
|
||||
P2000: (_operation, meta) =>
|
||||
new BadRequestException(
|
||||
`The provided value for ${
|
||||
meta?.target || 'a field'
|
||||
} is too long. Please use a shorter value.`,
|
||||
),
|
||||
|
||||
P2009: (operation) => new InternalServerErrorException(
|
||||
`Invalid data fetched during ${operation}. Check query structure.`
|
||||
),
|
||||
P2001: (operation, meta) =>
|
||||
new NotFoundException(
|
||||
`The ${
|
||||
meta?.model || 'record'
|
||||
} you are trying to ${operation} could not be found.`,
|
||||
),
|
||||
|
||||
P2010: () => new InternalServerErrorException(
|
||||
`Invalid raw query. Ensure your query is correct and try again.`
|
||||
),
|
||||
P2002: (operation, meta) => {
|
||||
const field = meta?.target || 'unique field';
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
return new ConflictException(
|
||||
`A record with the same ${field} already exists. Please use a different value.`,
|
||||
);
|
||||
case 'update':
|
||||
return new ConflictException(
|
||||
`The new value for ${field} conflicts with an existing record.`,
|
||||
);
|
||||
default:
|
||||
return new ConflictException(
|
||||
`Unique constraint violation on ${field}.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
P2011: (_operation, meta) => new BadRequestException(
|
||||
`The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.`
|
||||
),
|
||||
P2003: (operation) =>
|
||||
new BadRequestException(
|
||||
`Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`,
|
||||
),
|
||||
|
||||
P2012: (operation, meta) => new BadRequestException(
|
||||
`Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${operation}.`
|
||||
),
|
||||
P2006: (_operation, meta) =>
|
||||
new BadRequestException(
|
||||
`The provided value for ${
|
||||
meta?.target || 'a field'
|
||||
} is invalid. Please correct it.`,
|
||||
),
|
||||
|
||||
P2014: (operation) => {
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
return new BadRequestException(
|
||||
`Cannot create record because the referenced data does not exist. Ensure related data exists.`
|
||||
);
|
||||
case 'delete':
|
||||
return new BadRequestException(
|
||||
`Unable to delete record because it is linked to other data. Update or delete dependent records first.`
|
||||
);
|
||||
default:
|
||||
return new BadRequestException(`Foreign key constraint error.`);
|
||||
}
|
||||
},
|
||||
P2007: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Data validation error during ${operation}. Please ensure all inputs are valid and try again.`,
|
||||
),
|
||||
|
||||
P2015: () => new InternalServerErrorException(
|
||||
`A record with the required ID was expected but not found. Please retry.`
|
||||
),
|
||||
P2008: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Failed to query the database during ${operation}. Please try again later.`,
|
||||
),
|
||||
|
||||
P2016: (operation) => new InternalServerErrorException(
|
||||
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`
|
||||
),
|
||||
P2009: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Invalid data fetched during ${operation}. Check query structure.`,
|
||||
),
|
||||
|
||||
P2017: (operation) => new InternalServerErrorException(
|
||||
`Connected records were not found for ${operation}. Check related data.`
|
||||
),
|
||||
P2010: () =>
|
||||
new InternalServerErrorException(
|
||||
`Invalid raw query. Ensure your query is correct and try again.`,
|
||||
),
|
||||
|
||||
P2018: () => new InternalServerErrorException(
|
||||
`The required connection could not be established. Please check relationships.`
|
||||
),
|
||||
P2011: (_operation, meta) =>
|
||||
new BadRequestException(
|
||||
`The required field ${
|
||||
meta?.target || 'a field'
|
||||
} is missing. Please provide it to continue.`,
|
||||
),
|
||||
|
||||
P2019: (_operation, meta) => new InternalServerErrorException(
|
||||
`Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.`
|
||||
),
|
||||
P2012: (operation, meta) =>
|
||||
new BadRequestException(
|
||||
`Missing required relation ${
|
||||
meta?.relationName || ''
|
||||
}. Ensure all related data exists before ${operation}.`,
|
||||
),
|
||||
|
||||
P2021: (_operation, meta) => new InternalServerErrorException(
|
||||
`The ${meta?.model || 'model'} was not found in the database.`
|
||||
),
|
||||
P2014: (operation) => {
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
return new BadRequestException(
|
||||
`Cannot create record because the referenced data does not exist. Ensure related data exists.`,
|
||||
);
|
||||
case 'delete':
|
||||
return new BadRequestException(
|
||||
`Unable to delete record because it is linked to other data. Update or delete dependent records first.`,
|
||||
);
|
||||
default:
|
||||
return new BadRequestException(`Foreign key constraint error.`);
|
||||
}
|
||||
},
|
||||
|
||||
P2025: (operation, meta) => new NotFoundException(
|
||||
`The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.`
|
||||
),
|
||||
P2015: () =>
|
||||
new InternalServerErrorException(
|
||||
`A record with the required ID was expected but not found. Please retry.`,
|
||||
),
|
||||
|
||||
P2031: () => new InternalServerErrorException(
|
||||
`Invalid Prisma Client initialization error. Please check configuration.`
|
||||
),
|
||||
P2016: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`,
|
||||
),
|
||||
|
||||
P2033: (operation) => new InternalServerErrorException(
|
||||
`Insufficient database write permissions for ${operation}.`
|
||||
),
|
||||
P2017: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Connected records were not found for ${operation}. Check related data.`,
|
||||
),
|
||||
|
||||
P2034: (operation) => new InternalServerErrorException(
|
||||
`Database read-only transaction failed during ${operation}.`
|
||||
),
|
||||
P2018: () =>
|
||||
new InternalServerErrorException(
|
||||
`The required connection could not be established. Please check relationships.`,
|
||||
),
|
||||
|
||||
P2037: (operation) => new InternalServerErrorException(
|
||||
`Unsupported combinations of input types for ${operation}. Please correct the query or input.`
|
||||
),
|
||||
P2019: (_operation, meta) =>
|
||||
new InternalServerErrorException(
|
||||
`Invalid input for ${
|
||||
meta?.details || 'a field'
|
||||
}. Please ensure data conforms to expectations.`,
|
||||
),
|
||||
|
||||
P1000: () => new InternalServerErrorException(
|
||||
`Database authentication failed. Verify your credentials and try again.`
|
||||
),
|
||||
P2021: (_operation, meta) =>
|
||||
new InternalServerErrorException(
|
||||
`The ${meta?.model || 'model'} was not found in the database.`,
|
||||
),
|
||||
|
||||
P1001: () => new InternalServerErrorException(
|
||||
`The database server could not be reached. Please check its availability.`
|
||||
),
|
||||
P2025: (operation, meta) =>
|
||||
new NotFoundException(
|
||||
`The ${
|
||||
meta?.model || 'record'
|
||||
} you are trying to ${operation} does not exist. It may have been deleted.`,
|
||||
),
|
||||
|
||||
P1002: () => new InternalServerErrorException(
|
||||
`Connection to the database timed out. Verify network connectivity and server availability.`
|
||||
),
|
||||
P2031: () =>
|
||||
new InternalServerErrorException(
|
||||
`Invalid Prisma Client initialization error. Please check configuration.`,
|
||||
),
|
||||
|
||||
P1015: (operation) => new InternalServerErrorException(
|
||||
`Migration failed. Unable to complete ${operation}. Check migration history or database state.`
|
||||
),
|
||||
P2033: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Insufficient database write permissions for ${operation}.`,
|
||||
),
|
||||
|
||||
P1017: () => new InternalServerErrorException(
|
||||
`Database connection failed. Ensure the database is online and credentials are correct.`
|
||||
),
|
||||
P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
};
|
||||
|
||||
P2034: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Database read-only transaction failed during ${operation}.`,
|
||||
),
|
||||
|
||||
P2037: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Unsupported combinations of input types for ${operation}. Please correct the query or input.`,
|
||||
),
|
||||
|
||||
P1000: () =>
|
||||
new InternalServerErrorException(
|
||||
`Database authentication failed. Verify your credentials and try again.`,
|
||||
),
|
||||
|
||||
P1001: () =>
|
||||
new InternalServerErrorException(
|
||||
`The database server could not be reached. Please check its availability.`,
|
||||
),
|
||||
|
||||
P1002: () =>
|
||||
new InternalServerErrorException(
|
||||
`Connection to the database timed out. Verify network connectivity and server availability.`,
|
||||
),
|
||||
|
||||
P1015: (operation) =>
|
||||
new InternalServerErrorException(
|
||||
`Migration failed. Unable to complete ${operation}. Check migration history or database state.`,
|
||||
),
|
||||
|
||||
P1017: () =>
|
||||
new InternalServerErrorException(
|
||||
`Database connection failed. Ensure the database is online and credentials are correct.`,
|
||||
),
|
||||
P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,183 +1,176 @@
|
|||
import { UserProfile, RowModelRequest, RowRequestSchema } from "@nice/common";
|
||||
import { RowModelService } from "./row-model.service";
|
||||
import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder";
|
||||
import EventBus from "@server/utils/event-bus";
|
||||
import supejson from "superjson-cjs"
|
||||
import { deleteByPattern } from "@server/utils/redis/utils";
|
||||
import { redis } from "@server/utils/redis/redis.service";
|
||||
import { z } from "zod";
|
||||
import { UserProfile, RowModelRequest, RowRequestSchema } from '@nice/common';
|
||||
import { RowModelService } from './row-model.service';
|
||||
import { isFieldCondition, LogicalCondition, SQLBuilder } from './sql-builder';
|
||||
import EventBus from '@server/utils/event-bus';
|
||||
import supejson from 'superjson-cjs';
|
||||
import { deleteByPattern } from '@server/utils/redis/utils';
|
||||
import { redis } from '@server/utils/redis/redis.service';
|
||||
import { z } from 'zod';
|
||||
export class RowCacheService extends RowModelService {
|
||||
constructor(tableName: string, private enableCache: boolean = true) {
|
||||
super(tableName)
|
||||
if (this.enableCache) {
|
||||
EventBus.on("dataChanged", async ({ type, data }) => {
|
||||
if (type === tableName) {
|
||||
const dataArray = Array.isArray(data) ? data : [data];
|
||||
for (const item of dataArray) {
|
||||
try {
|
||||
if (item.id) {
|
||||
this.invalidateRowCacheById(item.id)
|
||||
}
|
||||
if (item.parentId) {
|
||||
this.invalidateRowCacheById(item.parentId)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error deleting cache for type ${tableName}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
constructor(
|
||||
tableName: string,
|
||||
private enableCache: boolean = true,
|
||||
) {
|
||||
super(tableName);
|
||||
if (this.enableCache) {
|
||||
EventBus.on('dataChanged', async ({ type, data }) => {
|
||||
if (type === tableName) {
|
||||
const dataArray = Array.isArray(data) ? data : [data];
|
||||
for (const item of dataArray) {
|
||||
try {
|
||||
if (item.id) {
|
||||
this.invalidateRowCacheById(item.id);
|
||||
}
|
||||
if (item.parentId) {
|
||||
this.invalidateRowCacheById(item.parentId);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error deleting cache for type ${tableName}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
protected getRowCacheKey(id: string) {
|
||||
return `row-data-${id}`;
|
||||
}
|
||||
protected getRowCacheKey(id: string) {
|
||||
return `row-data-${id}`;
|
||||
}
|
||||
private async invalidateRowCacheById(id: string) {
|
||||
if (!this.enableCache) return;
|
||||
const pattern = this.getRowCacheKey(id);
|
||||
await deleteByPattern(pattern);
|
||||
}
|
||||
createJoinSql(request?: RowModelRequest): string[] {
|
||||
return [];
|
||||
}
|
||||
protected async getRowRelation(args: { data: any; staff?: UserProfile }) {
|
||||
return args.data;
|
||||
}
|
||||
protected async setResPermissions(data: any, staff?: UserProfile) {
|
||||
return data;
|
||||
}
|
||||
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
|
||||
// 如果没有id,直接返回原数据
|
||||
if (!data?.id) return data;
|
||||
// 如果未启用缓存,直接处理并返回数据
|
||||
if (!this.enableCache) {
|
||||
return this.processDataWithPermissions(data, staff);
|
||||
}
|
||||
private async invalidateRowCacheById(id: string) {
|
||||
if (!this.enableCache) return;
|
||||
const pattern = this.getRowCacheKey(id);
|
||||
await deleteByPattern(pattern);
|
||||
}
|
||||
createJoinSql(request?: RowModelRequest): string[] {
|
||||
return []
|
||||
}
|
||||
protected async getRowRelation(args: { data: any, staff?: UserProfile }) {
|
||||
return args.data;
|
||||
}
|
||||
protected async setResPermissions(
|
||||
data: any,
|
||||
staff?: UserProfile,
|
||||
) {
|
||||
return data
|
||||
}
|
||||
protected async getRowDto(
|
||||
data: any,
|
||||
staff?: UserProfile,
|
||||
): Promise<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;
|
||||
}
|
||||
const key = this.getRowCacheKey(data.id);
|
||||
try {
|
||||
// 尝试从缓存获取数据
|
||||
const cachedData = await this.getCachedData(key, staff);
|
||||
// 如果缓存命中,直接返回
|
||||
if (cachedData) return cachedData;
|
||||
// 处理数据并缓存
|
||||
const processedData = await this.processDataWithPermissions(data, staff);
|
||||
await redis.set(key, supejson.stringify(processedData));
|
||||
return processedData;
|
||||
} catch (err) {
|
||||
this.logger.error('Error in getRowDto:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async getCachedData(
|
||||
key: string,
|
||||
staff?: UserProfile
|
||||
): Promise<any | null> {
|
||||
const cachedDataStr = await redis.get(key);
|
||||
if (!cachedDataStr) return null;
|
||||
const cachedData = supejson.parse(cachedDataStr) as any;
|
||||
if (!cachedData?.id) return null;
|
||||
return staff
|
||||
? this.setResPermissions(cachedData, staff)
|
||||
: cachedData;
|
||||
}
|
||||
private async getCachedData(
|
||||
key: string,
|
||||
staff?: UserProfile,
|
||||
): Promise<any | null> {
|
||||
const cachedDataStr = await redis.get(key);
|
||||
if (!cachedDataStr) return null;
|
||||
const cachedData = supejson.parse(cachedDataStr) as any;
|
||||
if (!cachedData?.id) return null;
|
||||
return staff ? this.setResPermissions(cachedData, staff) : cachedData;
|
||||
}
|
||||
|
||||
private async processDataWithPermissions(
|
||||
data: any,
|
||||
staff?: UserProfile
|
||||
): Promise<any> {
|
||||
// 处理权限
|
||||
const permData = staff
|
||||
? await this.setResPermissions(data, staff)
|
||||
: data;
|
||||
// 获取关联数据
|
||||
return this.getRowRelation({ data: permData, staff });
|
||||
}
|
||||
private async processDataWithPermissions(
|
||||
data: any,
|
||||
staff?: UserProfile,
|
||||
): Promise<any> {
|
||||
// 处理权限
|
||||
const permData = staff ? await this.setResPermissions(data, staff) : data;
|
||||
// 获取关联数据
|
||||
return this.getRowRelation({ data: permData, staff });
|
||||
}
|
||||
|
||||
protected createGetRowsFilters(
|
||||
request: z.infer<typeof RowRequestSchema>,
|
||||
staff?: UserProfile,
|
||||
) {
|
||||
const condition = super.createGetRowsFilters(request);
|
||||
if (isFieldCondition(condition)) return {};
|
||||
const baseCondition: LogicalCondition[] = [
|
||||
{
|
||||
field: `${this.tableName}.deleted_at`,
|
||||
op: 'blank',
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
condition.AND = [...baseCondition, ...condition.AND];
|
||||
return condition;
|
||||
}
|
||||
createUnGroupingRowSelect(request?: RowModelRequest): string[] {
|
||||
return [
|
||||
`${this.tableName}.id AS id`,
|
||||
SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
|
||||
];
|
||||
}
|
||||
protected createGroupingRowSelect(
|
||||
request: RowModelRequest,
|
||||
wrapperSql: boolean,
|
||||
): string[] {
|
||||
const colsToSelect = super.createGroupingRowSelect(request, wrapperSql);
|
||||
return colsToSelect.concat([
|
||||
SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
|
||||
]);
|
||||
}
|
||||
protected async getRowsSqlWrapper(
|
||||
sql: string,
|
||||
request?: RowModelRequest,
|
||||
staff?: UserProfile,
|
||||
): Promise<string> {
|
||||
const groupingSql = SQLBuilder.join([
|
||||
SQLBuilder.select([
|
||||
...this.createGroupingRowSelect(request, true),
|
||||
`${this.tableName}.id AS id`,
|
||||
]),
|
||||
SQLBuilder.from(this.tableName),
|
||||
SQLBuilder.join(this.createJoinSql(request)),
|
||||
SQLBuilder.where(this.createGetRowsFilters(request, staff)),
|
||||
]);
|
||||
const { rowGroupCols, valueCols, groupKeys } = request;
|
||||
if (this.isDoingGroup(request)) {
|
||||
const rowGroupCol = rowGroupCols[groupKeys.length];
|
||||
const groupByField = rowGroupCol?.field?.replace('.', '_');
|
||||
return SQLBuilder.join([
|
||||
SQLBuilder.select([
|
||||
groupByField,
|
||||
...super.createAggSqlForWrapper(request),
|
||||
'COUNT(id) AS child_count',
|
||||
]),
|
||||
SQLBuilder.from(`(${groupingSql})`),
|
||||
SQLBuilder.where({
|
||||
field: 'row_num',
|
||||
value: '1',
|
||||
op: 'equals',
|
||||
}),
|
||||
SQLBuilder.groupBy([groupByField]),
|
||||
SQLBuilder.orderBy(
|
||||
this.getOrderByColumns(request).map((item) => item.replace('.', '_')),
|
||||
),
|
||||
this.getLimitSql(request),
|
||||
]);
|
||||
} else
|
||||
return SQLBuilder.join([
|
||||
SQLBuilder.select(['*']),
|
||||
SQLBuilder.from(`(${sql})`),
|
||||
SQLBuilder.where({
|
||||
field: 'row_num',
|
||||
value: '1',
|
||||
op: 'equals',
|
||||
}),
|
||||
this.getLimitSql(request),
|
||||
]);
|
||||
// return super.getRowsSqlWrapper(sql, request)
|
||||
}
|
||||
}
|
||||
protected createGetRowsFilters(
|
||||
request: z.infer<typeof RowRequestSchema>,
|
||||
staff?: UserProfile,
|
||||
) {
|
||||
const condition = super.createGetRowsFilters(request);
|
||||
if (isFieldCondition(condition)) return {};
|
||||
const baseCondition: LogicalCondition[] = [
|
||||
{
|
||||
field: `${this.tableName}.deleted_at`,
|
||||
op: 'blank',
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
condition.AND = [...baseCondition, ...condition.AND];
|
||||
return condition;
|
||||
}
|
||||
createUnGroupingRowSelect(request?: RowModelRequest): string[] {
|
||||
return [
|
||||
`${this.tableName}.id AS id`,
|
||||
SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
|
||||
];
|
||||
}
|
||||
protected createGroupingRowSelect(
|
||||
request: RowModelRequest,
|
||||
wrapperSql: boolean,
|
||||
): string[] {
|
||||
const colsToSelect = super.createGroupingRowSelect(request, wrapperSql);
|
||||
return colsToSelect.concat([
|
||||
SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
|
||||
]);
|
||||
}
|
||||
protected async getRowsSqlWrapper(
|
||||
sql: string,
|
||||
request?: RowModelRequest,
|
||||
staff?: UserProfile,
|
||||
): Promise<string> {
|
||||
const groupingSql = SQLBuilder.join([
|
||||
SQLBuilder.select([
|
||||
...this.createGroupingRowSelect(request, true),
|
||||
`${this.tableName}.id AS id`,
|
||||
]),
|
||||
SQLBuilder.from(this.tableName),
|
||||
SQLBuilder.join(this.createJoinSql(request)),
|
||||
SQLBuilder.where(this.createGetRowsFilters(request, staff)),
|
||||
]);
|
||||
const { rowGroupCols, valueCols, groupKeys } = request;
|
||||
if (this.isDoingGroup(request)) {
|
||||
const rowGroupCol = rowGroupCols[groupKeys.length];
|
||||
const groupByField = rowGroupCol?.field?.replace('.', '_');
|
||||
return SQLBuilder.join([
|
||||
SQLBuilder.select([
|
||||
groupByField,
|
||||
...super.createAggSqlForWrapper(request),
|
||||
'COUNT(id) AS child_count',
|
||||
]),
|
||||
SQLBuilder.from(`(${groupingSql})`),
|
||||
SQLBuilder.where({
|
||||
field: 'row_num',
|
||||
value: '1',
|
||||
op: 'equals',
|
||||
}),
|
||||
SQLBuilder.groupBy([groupByField]),
|
||||
SQLBuilder.orderBy(
|
||||
this.getOrderByColumns(request).map((item) => item.replace('.', '_')),
|
||||
),
|
||||
this.getLimitSql(request),
|
||||
]);
|
||||
} else
|
||||
return SQLBuilder.join([
|
||||
SQLBuilder.select(['*']),
|
||||
SQLBuilder.from(`(${sql})`),
|
||||
SQLBuilder.where({
|
||||
field: 'row_num',
|
||||
value: '1',
|
||||
op: 'equals',
|
||||
}),
|
||||
this.getLimitSql(request),
|
||||
]);
|
||||
// return super.getRowsSqlWrapper(sql, request)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export abstract class RowModelService {
|
|||
// 添加更多需要引号的关键词
|
||||
]);
|
||||
protected logger = new Logger(this.tableName);
|
||||
protected constructor(protected tableName: string) { }
|
||||
protected constructor(protected tableName: string) {}
|
||||
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
|
||||
return row;
|
||||
}
|
||||
|
@ -140,11 +140,11 @@ export abstract class RowModelService {
|
|||
private buildFilterConditions(filterModel: any): LogicalCondition[] {
|
||||
return filterModel
|
||||
? Object.entries(filterModel)?.map(([key, item]) =>
|
||||
SQLBuilder.createFilterSql(
|
||||
key === 'ag-Grid-AutoColumn' ? 'name' : key,
|
||||
item,
|
||||
),
|
||||
)
|
||||
SQLBuilder.createFilterSql(
|
||||
key === 'ag-Grid-AutoColumn' ? 'name' : key,
|
||||
item,
|
||||
),
|
||||
)
|
||||
: [];
|
||||
}
|
||||
|
||||
|
@ -160,7 +160,10 @@ export abstract class RowModelService {
|
|||
const { rowGroupCols, valueCols, groupKeys } = request;
|
||||
return valueCols.map(
|
||||
(valueCol) =>
|
||||
`${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}`,
|
||||
`${valueCol.aggFunc}(${valueCol.field.replace(
|
||||
'.',
|
||||
'_',
|
||||
)}) AS ${valueCol.field.split('.').join('_')}`,
|
||||
);
|
||||
}
|
||||
protected createGroupingRowSelect(
|
||||
|
@ -179,7 +182,9 @@ export abstract class RowModelService {
|
|||
colsToSelect.push(
|
||||
...valueCols.map(
|
||||
(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[] {
|
||||
return valueCols.map(
|
||||
(valueCol) =>
|
||||
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`,
|
||||
`${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace(
|
||||
'.',
|
||||
'_',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,138 +1,172 @@
|
|||
export interface FieldCondition {
|
||||
field: string;
|
||||
op: OperatorType
|
||||
type?: "text" | "number" | "date";
|
||||
value?: any;
|
||||
valueTo?: any;
|
||||
};
|
||||
export type OperatorType = 'equals' | 'notEqual' | 'contains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank' | 'greaterThan' | 'lessThanOrEqual' | 'inRange' | 'lessThan' | 'greaterThan' | 'in';
|
||||
export type LogicalCondition = FieldCondition | {
|
||||
AND?: LogicalCondition[];
|
||||
OR?: LogicalCondition[];
|
||||
};
|
||||
field: string;
|
||||
op: OperatorType;
|
||||
type?: 'text' | 'number' | 'date';
|
||||
value?: any;
|
||||
valueTo?: any;
|
||||
}
|
||||
export type OperatorType =
|
||||
| 'equals'
|
||||
| 'notEqual'
|
||||
| 'contains'
|
||||
| 'startsWith'
|
||||
| 'endsWith'
|
||||
| 'blank'
|
||||
| 'notBlank'
|
||||
| 'greaterThan'
|
||||
| 'lessThanOrEqual'
|
||||
| 'inRange'
|
||||
| 'lessThan'
|
||||
| 'greaterThan'
|
||||
| 'in';
|
||||
export type LogicalCondition =
|
||||
| FieldCondition
|
||||
| {
|
||||
AND?: LogicalCondition[];
|
||||
OR?: LogicalCondition[];
|
||||
};
|
||||
|
||||
export function isFieldCondition(condition: LogicalCondition): condition is FieldCondition {
|
||||
return (condition as FieldCondition).field !== undefined;
|
||||
export function isFieldCondition(
|
||||
condition: LogicalCondition,
|
||||
): condition is FieldCondition {
|
||||
return (condition as FieldCondition).field !== undefined;
|
||||
}
|
||||
function buildCondition(condition: FieldCondition): string {
|
||||
const { field, op, value, type = "text", valueTo } = condition;
|
||||
switch (op) {
|
||||
case 'equals':
|
||||
return `${field} = '${value}'`;
|
||||
case 'notEqual':
|
||||
return `${field} != '${value}'`;
|
||||
case 'contains':
|
||||
return `${field} LIKE '%${value}%'`;
|
||||
case 'startsWith':
|
||||
return `${field} LIKE '${value}%'`;
|
||||
case 'endsWith':
|
||||
return `${field} LIKE '%${value}'`;
|
||||
case 'blank':
|
||||
if (type !== "date")
|
||||
return `(${field} IS NULL OR ${field} = '')`;
|
||||
else
|
||||
return `${field} IS NULL`;
|
||||
case 'notBlank':
|
||||
if (type !== 'date')
|
||||
return `${field} IS NOT NULL AND ${field} != ''`;
|
||||
else
|
||||
return `${field} IS NOT NULL`;
|
||||
case 'greaterThan':
|
||||
return `${field} > '${value}'`;
|
||||
case 'lessThanOrEqual':
|
||||
return `${field} <= '${value}'`;
|
||||
case 'lessThan':
|
||||
return `${field} < '${value}'`;
|
||||
case 'greaterThan':
|
||||
return `${field} > '${value}'`;
|
||||
case 'inRange':
|
||||
return `${field} >= '${value}' AND ${field} <= '${valueTo}'`;
|
||||
case 'in':
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
// Return a condition that is always false if value is empty or an empty array
|
||||
return '1 = 0';
|
||||
}
|
||||
return `${field} IN (${(value as any[]).map(val => `'${val}'`).join(', ')})`;
|
||||
default:
|
||||
return 'true'; // Default return for unmatched conditions
|
||||
}
|
||||
const { field, op, value, type = 'text', valueTo } = condition;
|
||||
switch (op) {
|
||||
case 'equals':
|
||||
return `${field} = '${value}'`;
|
||||
case 'notEqual':
|
||||
return `${field} != '${value}'`;
|
||||
case 'contains':
|
||||
return `${field} LIKE '%${value}%'`;
|
||||
case 'startsWith':
|
||||
return `${field} LIKE '${value}%'`;
|
||||
case 'endsWith':
|
||||
return `${field} LIKE '%${value}'`;
|
||||
case 'blank':
|
||||
if (type !== 'date') return `(${field} IS NULL OR ${field} = '')`;
|
||||
else return `${field} IS NULL`;
|
||||
case 'notBlank':
|
||||
if (type !== 'date') return `${field} IS NOT NULL AND ${field} != ''`;
|
||||
else return `${field} IS NOT NULL`;
|
||||
case 'greaterThan':
|
||||
return `${field} > '${value}'`;
|
||||
case 'lessThanOrEqual':
|
||||
return `${field} <= '${value}'`;
|
||||
case 'lessThan':
|
||||
return `${field} < '${value}'`;
|
||||
case 'greaterThan':
|
||||
return `${field} > '${value}'`;
|
||||
case 'inRange':
|
||||
return `${field} >= '${value}' AND ${field} <= '${valueTo}'`;
|
||||
case 'in':
|
||||
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||
// Return a condition that is always false if value is empty or an empty array
|
||||
return '1 = 0';
|
||||
}
|
||||
return `${field} IN (${(value as any[])
|
||||
.map((val) => `'${val}'`)
|
||||
.join(', ')})`;
|
||||
default:
|
||||
return 'true'; // Default return for unmatched conditions
|
||||
}
|
||||
}
|
||||
function buildLogicalCondition(logicalCondition: LogicalCondition): string {
|
||||
if (isFieldCondition(logicalCondition)) {
|
||||
return buildCondition(logicalCondition);
|
||||
if (isFieldCondition(logicalCondition)) {
|
||||
return buildCondition(logicalCondition);
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (logicalCondition.AND && logicalCondition.AND.length > 0) {
|
||||
const andParts = logicalCondition.AND.map((c) =>
|
||||
buildLogicalCondition(c),
|
||||
).filter((part) => part !== ''); // Filter out empty conditions
|
||||
if (andParts.length > 0) {
|
||||
parts.push(`(${andParts.join(' AND ')})`);
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (logicalCondition.AND && logicalCondition.AND.length > 0) {
|
||||
const andParts = logicalCondition.AND
|
||||
.map(c => buildLogicalCondition(c))
|
||||
.filter(part => part !== ''); // Filter out empty conditions
|
||||
if (andParts.length > 0) {
|
||||
parts.push(`(${andParts.join(' AND ')})`);
|
||||
}
|
||||
}
|
||||
// Process OR conditions
|
||||
if (logicalCondition.OR && logicalCondition.OR.length > 0) {
|
||||
const orParts = logicalCondition.OR.map((c) =>
|
||||
buildLogicalCondition(c),
|
||||
).filter((part) => part !== ''); // Filter out empty conditions
|
||||
if (orParts.length > 0) {
|
||||
parts.push(`(${orParts.join(' OR ')})`);
|
||||
}
|
||||
// Process OR conditions
|
||||
if (logicalCondition.OR && logicalCondition.OR.length > 0) {
|
||||
const orParts = logicalCondition.OR
|
||||
.map(c => buildLogicalCondition(c))
|
||||
.filter(part => part !== ''); // Filter out empty conditions
|
||||
if (orParts.length > 0) {
|
||||
parts.push(`(${orParts.join(' OR ')})`);
|
||||
}
|
||||
}
|
||||
// Join AND and OR parts with an 'AND' if both are present
|
||||
return parts.length > 1 ? parts.join(' AND ') : parts[0] || '';
|
||||
}
|
||||
// Join AND and OR parts with an 'AND' if both are present
|
||||
return parts.length > 1 ? parts.join(' AND ') : parts[0] || '';
|
||||
}
|
||||
|
||||
export class SQLBuilder {
|
||||
static select(fields: string[], distinctField?: string): string {
|
||||
const distinctClause = distinctField ? `DISTINCT ON (${distinctField}) ` : "";
|
||||
return `SELECT ${distinctClause}${fields.join(", ")}`;
|
||||
}
|
||||
static rowNumber(orderBy: string, partitionBy: string | null = null, alias: string = 'row_num'): string {
|
||||
if (!orderBy) {
|
||||
throw new Error("orderBy 参数不能为空");
|
||||
}
|
||||
|
||||
let partitionClause = '';
|
||||
if (partitionBy) {
|
||||
partitionClause = `PARTITION BY ${partitionBy} `;
|
||||
}
|
||||
|
||||
return `ROW_NUMBER() OVER (${partitionClause}ORDER BY ${orderBy}) AS ${alias}`;
|
||||
}
|
||||
static from(tableName: string): string {
|
||||
return `FROM ${tableName}`;
|
||||
static select(fields: string[], distinctField?: string): string {
|
||||
const distinctClause = distinctField
|
||||
? `DISTINCT ON (${distinctField}) `
|
||||
: '';
|
||||
return `SELECT ${distinctClause}${fields.join(', ')}`;
|
||||
}
|
||||
static rowNumber(
|
||||
orderBy: string,
|
||||
partitionBy: string | null = null,
|
||||
alias: string = 'row_num',
|
||||
): string {
|
||||
if (!orderBy) {
|
||||
throw new Error('orderBy 参数不能为空');
|
||||
}
|
||||
|
||||
static where(conditions: LogicalCondition): string {
|
||||
const whereClause = buildLogicalCondition(conditions);
|
||||
return whereClause ? `WHERE ${whereClause}` : "";
|
||||
let partitionClause = '';
|
||||
if (partitionBy) {
|
||||
partitionClause = `PARTITION BY ${partitionBy} `;
|
||||
}
|
||||
|
||||
static groupBy(columns: string[]): string {
|
||||
return columns.length ? `GROUP BY ${columns.join(", ")}` : "";
|
||||
}
|
||||
return `ROW_NUMBER() OVER (${partitionClause}ORDER BY ${orderBy}) AS ${alias}`;
|
||||
}
|
||||
static from(tableName: string): string {
|
||||
return `FROM ${tableName}`;
|
||||
}
|
||||
|
||||
static orderBy(columns: string[]): string {
|
||||
return columns.length ? `ORDER BY ${columns.join(", ")}` : "";
|
||||
}
|
||||
static where(conditions: LogicalCondition): string {
|
||||
const whereClause = buildLogicalCondition(conditions);
|
||||
return whereClause ? `WHERE ${whereClause}` : '';
|
||||
}
|
||||
|
||||
static limit(pageSize: number, offset: number = 0): string {
|
||||
return `LIMIT ${pageSize + 1} OFFSET ${offset}`;
|
||||
}
|
||||
static groupBy(columns: string[]): string {
|
||||
return columns.length ? `GROUP BY ${columns.join(', ')}` : '';
|
||||
}
|
||||
|
||||
static join(clauses: string[]): string {
|
||||
return clauses.filter(Boolean).join(' ');
|
||||
}
|
||||
static createFilterSql(key: string, item: any): LogicalCondition {
|
||||
const conditionFuncs: Record<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 orderBy(columns: string[]): string {
|
||||
return columns.length ? `ORDER BY ${columns.join(', ')}` : '';
|
||||
}
|
||||
|
||||
}
|
||||
static limit(pageSize: number, offset: number = 0): string {
|
||||
return `LIMIT ${pageSize + 1} OFFSET ${offset}`;
|
||||
}
|
||||
|
||||
static join(clauses: string[]): string {
|
||||
return clauses.filter(Boolean).join(' ');
|
||||
}
|
||||
static createFilterSql(key: string, item: any): LogicalCondition {
|
||||
const conditionFuncs: Record<
|
||||
string,
|
||||
(item: {
|
||||
values?: any[];
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
filter: any;
|
||||
type: OperatorType;
|
||||
filterType: OperatorType;
|
||||
}) => LogicalCondition
|
||||
> = {
|
||||
text: (item) => ({ value: item.filter, op: item.type, field: key }),
|
||||
number: (item) => ({ value: item.filter, op: item.type, field: key }),
|
||||
date: (item) => ({
|
||||
value: item.dateFrom,
|
||||
valueTo: item.dateTo,
|
||||
op: item.type,
|
||||
field: key,
|
||||
}),
|
||||
set: (item) => ({ value: item.values, op: 'in', field: key }),
|
||||
};
|
||||
return conditionFuncs[item.filterType](item);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Controller, UseGuards } from "@nestjs/common";
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@server/auth/auth.guard';
|
||||
import { DailyTrainService } from "./dailyTrain.service";
|
||||
import { DailyTrainService } from './dailyTrain.service';
|
||||
|
||||
@Controller('train-content')
|
||||
export class DailyTrainController {
|
||||
constructor(private readonly dailyTrainService: DailyTrainService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
constructor(private readonly dailyTrainService: DailyTrainService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
|
|
|
@ -5,11 +5,10 @@ import { DailyTrainController } from './dailyTrain.controller';
|
|||
import { DailyTrainService } from './dailyTrain.service';
|
||||
import { DailyTrainRouter } from './dailyTrain.router';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [StaffModule],
|
||||
controllers: [DailyTrainController],
|
||||
providers: [DailyTrainService,DailyTrainRouter,TrpcService],
|
||||
exports: [DailyTrainService,DailyTrainRouter],
|
||||
providers: [DailyTrainService, DailyTrainRouter, TrpcService],
|
||||
exports: [DailyTrainService, DailyTrainRouter],
|
||||
})
|
||||
export class DailyTrainModule {}
|
||||
export class DailyTrainModule {}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { TrpcService } from "@server/trpc/trpc.service";
|
||||
import { DailyTrainService } from "./dailyTrain.service";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { DailyTrainService } from './dailyTrain.service';
|
||||
|
||||
@Injectable()
|
||||
export class DailyTrainRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly dailyTrainService: DailyTrainService,
|
||||
) { }
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly dailyTrainService: DailyTrainService,
|
||||
) {}
|
||||
|
||||
router = this.trpc.router({
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
router = this.trpc.router({});
|
||||
}
|
||||
|
|
|
@ -1,40 +1,37 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseService } from "../base/base.service";
|
||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
|
||||
@Injectable()
|
||||
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
|
||||
constructor() {
|
||||
super(db,ObjectType.DAILY_TRAIN,true);
|
||||
}
|
||||
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
||||
console.log(args)
|
||||
const result = await super.create(args)
|
||||
this.emitDataChanged(CrudOperation.CREATED,result)
|
||||
return result
|
||||
}
|
||||
constructor() {
|
||||
super(db, ObjectType.DAILY_TRAIN, true);
|
||||
}
|
||||
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
||||
console.log(args);
|
||||
const result = await super.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(args:Prisma.DailyTrainTimeUpdateArgs){
|
||||
const result = await super.update(args)
|
||||
this.emitDataChanged(CrudOperation.UPDATED,result)
|
||||
return result
|
||||
}
|
||||
async update(args: Prisma.DailyTrainTimeUpdateArgs) {
|
||||
const result = await super.update(args);
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type:ObjectType.DAILY_TRAIN,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.DAILY_TRAIN,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { db } from '@nice/common';
|
|||
|
||||
@Controller('dept')
|
||||
export class DepartmentController {
|
||||
constructor(private readonly deptService: DepartmentService) { }
|
||||
constructor(private readonly deptService: DepartmentService) {}
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('get-detail')
|
||||
async getDepartmentDetails(@Query('dept-id') deptId: string) {
|
||||
|
|
|
@ -6,8 +6,13 @@ import { DepartmentController } from './department.controller';
|
|||
import { DepartmentRowService } from './department.row.service';
|
||||
|
||||
@Module({
|
||||
providers: [DepartmentService, DepartmentRouter, DepartmentRowService, TrpcService],
|
||||
exports: [DepartmentService, DepartmentRouter],
|
||||
controllers: [DepartmentController],
|
||||
providers: [
|
||||
DepartmentService,
|
||||
DepartmentRouter,
|
||||
DepartmentRowService,
|
||||
TrpcService,
|
||||
],
|
||||
exports: [DepartmentService, DepartmentRouter],
|
||||
controllers: [DepartmentController],
|
||||
})
|
||||
export class DepartmentModule { }
|
||||
export class DepartmentModule {}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { db, VisitType } from '@nice/common';
|
|||
|
||||
@Controller('message')
|
||||
export class MessageController {
|
||||
constructor(private readonly messageService: MessageService) { }
|
||||
constructor(private readonly messageService: MessageService) {}
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('find-last-one')
|
||||
async findLastOne(@Query('staff-id') staffId: string) {
|
||||
|
@ -27,7 +27,7 @@ export class MessageController {
|
|||
select: {
|
||||
title: true,
|
||||
content: true,
|
||||
url: true
|
||||
url: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -53,7 +53,7 @@ export class MessageController {
|
|||
visits: {
|
||||
none: {
|
||||
id: staffId,
|
||||
type: VisitType.READED
|
||||
type: VisitType.READED,
|
||||
},
|
||||
},
|
||||
receivers: {
|
||||
|
@ -92,7 +92,7 @@ export class MessageController {
|
|||
visits: {
|
||||
none: {
|
||||
id: staffId,
|
||||
type: VisitType.READED
|
||||
type: VisitType.READED,
|
||||
},
|
||||
},
|
||||
receivers: {
|
||||
|
|
|
@ -11,4 +11,4 @@ import { MessageController } from './message.controller';
|
|||
exports: [MessageService, MessageRouter],
|
||||
controllers: [MessageController],
|
||||
})
|
||||
export class MessageModule { }
|
||||
export class MessageModule {}
|
||||
|
|
|
@ -3,15 +3,16 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
|||
import { MessageService } from './message.service';
|
||||
import { Prisma } from '@nice/common';
|
||||
import { z, ZodType } from 'zod';
|
||||
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> = z.any()
|
||||
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any()
|
||||
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any()
|
||||
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> =
|
||||
z.any();
|
||||
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any();
|
||||
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any();
|
||||
@Injectable()
|
||||
export class MessageRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly messageService: MessageService,
|
||||
) { }
|
||||
) {}
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.procedure
|
||||
.input(MessageUncheckedCreateInputSchema)
|
||||
|
@ -20,20 +21,21 @@ export class MessageRouter {
|
|||
return await this.messageService.create({ data: input }, { staff });
|
||||
}),
|
||||
findManyWithCursor: this.trpc.protectProcedure
|
||||
.input(z.object({
|
||||
cursor: z.any().nullish(),
|
||||
take: z.number().nullish(),
|
||||
where: MessageWhereInputSchema.nullish(),
|
||||
select: MessageSelectSchema.nullish()
|
||||
}))
|
||||
.input(
|
||||
z.object({
|
||||
cursor: z.any().nullish(),
|
||||
take: z.number().nullish(),
|
||||
where: MessageWhereInputSchema.nullish(),
|
||||
select: MessageSelectSchema.nullish(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.messageService.findManyWithCursor(input, staff);
|
||||
}),
|
||||
getUnreadCount: this.trpc.protectProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.messageService.getUnreadCount(staff);
|
||||
})
|
||||
})
|
||||
getUnreadCount: this.trpc.protectProcedure.query(async ({ ctx }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.messageService.getUnreadCount(staff);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,26 +8,28 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
|
|||
constructor() {
|
||||
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.include = {
|
||||
receivers: {
|
||||
select: { id: true, registerToken: true, username: true }
|
||||
}
|
||||
}
|
||||
select: { id: true, registerToken: true, username: true },
|
||||
},
|
||||
};
|
||||
const result = await super.create(args);
|
||||
EventBus.emit("dataChanged", {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.MESSAGE,
|
||||
operation: CrudOperation.CREATED,
|
||||
data: result
|
||||
})
|
||||
return result
|
||||
data: result,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
async findManyWithCursor(
|
||||
args: Prisma.MessageFindManyArgs,
|
||||
staff?: UserProfile,
|
||||
) {
|
||||
|
||||
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
||||
let { items } = result;
|
||||
await Promise.all(
|
||||
|
@ -46,12 +48,12 @@ export class MessageService extends BaseService<Prisma.MessageDelegate> {
|
|||
visits: {
|
||||
none: {
|
||||
visitorId: staff?.id,
|
||||
type: VisitType.READED
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
type: VisitType.READED,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return count
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
import { Message, UserProfile, VisitType, db } from "@nice/common"
|
||||
import { Message, UserProfile, VisitType, db } from '@nice/common';
|
||||
export async function setMessageRelation(
|
||||
data: Message,
|
||||
staff?: UserProfile,
|
||||
data: Message,
|
||||
staff?: UserProfile,
|
||||
): Promise<any> {
|
||||
const readed =
|
||||
(await db.visit.count({
|
||||
where: {
|
||||
messageId: data.id,
|
||||
type: VisitType.READED,
|
||||
visitorId: staff?.id,
|
||||
},
|
||||
})) > 0;
|
||||
|
||||
const readed =
|
||||
(await db.visit.count({
|
||||
where: {
|
||||
messageId: data.id,
|
||||
type: VisitType.READED,
|
||||
visitorId: staff?.id,
|
||||
},
|
||||
})) > 0;
|
||||
|
||||
|
||||
Object.assign(data, {
|
||||
readed
|
||||
})
|
||||
}
|
||||
Object.assign(data, {
|
||||
readed,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Controller, UseGuards } from "@nestjs/common";
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@server/auth/auth.guard';
|
||||
import { DailyTrainService } from "./dailyTrain.service";
|
||||
import { DailyTrainService } from './dailyTrain.service';
|
||||
|
||||
@Controller('train-content')
|
||||
export class DailyTrainController {
|
||||
constructor(private readonly dailyTrainService: DailyTrainService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
constructor(private readonly dailyTrainService: DailyTrainService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
|
|
|
@ -5,11 +5,10 @@ import { DailyTrainController } from './dailyTrain.controller';
|
|||
import { DailyTrainService } from './dailyTrain.service';
|
||||
import { DailyTrainRouter } from './dailyTrain.router';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [StaffModule],
|
||||
controllers: [DailyTrainController],
|
||||
providers: [DailyTrainService,DailyTrainRouter,TrpcService],
|
||||
exports: [DailyTrainService,DailyTrainRouter],
|
||||
providers: [DailyTrainService, DailyTrainRouter, TrpcService],
|
||||
exports: [DailyTrainService, DailyTrainRouter],
|
||||
})
|
||||
export class DailyTrainModule {}
|
||||
export class DailyTrainModule {}
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { TrpcService } from "@server/trpc/trpc.service";
|
||||
import { DailyTrainService } from "./dailyTrain.service";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { DailyTrainService } from './dailyTrain.service';
|
||||
|
||||
@Injectable()
|
||||
export class DailyTrainRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly dailyTrainService: DailyTrainService,
|
||||
) { }
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly dailyTrainService: DailyTrainService,
|
||||
) {}
|
||||
|
||||
router = this.trpc.router({
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
router = this.trpc.router({});
|
||||
}
|
||||
|
|
|
@ -1,40 +1,37 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseService } from "../base/base.service";
|
||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
|
||||
@Injectable()
|
||||
export class DailyTrainService extends BaseService<Prisma.DailyTrainTimeDelegate> {
|
||||
constructor() {
|
||||
super(db,ObjectType.DAILY_TRAIN,true);
|
||||
}
|
||||
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
||||
console.log(args)
|
||||
const result = await super.create(args)
|
||||
this.emitDataChanged(CrudOperation.CREATED,result)
|
||||
return result
|
||||
}
|
||||
constructor() {
|
||||
super(db, ObjectType.DAILY_TRAIN, true);
|
||||
}
|
||||
async create(args: Prisma.DailyTrainTimeCreateArgs) {
|
||||
console.log(args);
|
||||
const result = await super.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(args:Prisma.DailyTrainTimeUpdateArgs){
|
||||
const result = await super.update(args)
|
||||
this.emitDataChanged(CrudOperation.UPDATED,result)
|
||||
return result
|
||||
}
|
||||
async update(args: Prisma.DailyTrainTimeUpdateArgs) {
|
||||
const result = await super.update(args);
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.DailyTrainTimeFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type:ObjectType.DAILY_TRAIN,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.DAILY_TRAIN,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,13 @@ import { DepartmentModule } from '../department/department.module';
|
|||
|
||||
@Module({
|
||||
imports: [DepartmentModule],
|
||||
providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter],
|
||||
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter]
|
||||
providers: [
|
||||
RoleMapService,
|
||||
RoleRouter,
|
||||
TrpcService,
|
||||
RoleService,
|
||||
RoleMapRouter,
|
||||
],
|
||||
exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter],
|
||||
})
|
||||
export class RoleMapModule { }
|
||||
export class RoleMapModule {}
|
||||
|
|
|
@ -3,86 +3,91 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
|||
import { Prisma, UpdateOrderSchema } from '@nice/common';
|
||||
import { RoleService } from './role.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any()
|
||||
const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any()
|
||||
const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any()
|
||||
const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any()
|
||||
const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any()
|
||||
const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any()
|
||||
const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any()
|
||||
const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any()
|
||||
const RoleCreateArgsSchema: ZodType<Prisma.RoleCreateArgs> = z.any();
|
||||
const RoleUpdateArgsSchema: ZodType<Prisma.RoleUpdateArgs> = z.any();
|
||||
const RoleCreateManyInputSchema: ZodType<Prisma.RoleCreateManyInput> = z.any();
|
||||
const RoleDeleteManyArgsSchema: ZodType<Prisma.RoleDeleteManyArgs> = z.any();
|
||||
const RoleFindManyArgsSchema: ZodType<Prisma.RoleFindManyArgs> = z.any();
|
||||
const RoleFindFirstArgsSchema: ZodType<Prisma.RoleFindFirstArgs> = z.any();
|
||||
const RoleWhereInputSchema: ZodType<Prisma.RoleWhereInput> = z.any();
|
||||
const RoleSelectSchema: ZodType<Prisma.RoleSelect> = z.any();
|
||||
const RoleUpdateInputSchema: ZodType<Prisma.RoleUpdateInput> = z.any();
|
||||
@Injectable()
|
||||
export class RoleRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly roleService: RoleService,
|
||||
) { }
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.protectProcedure
|
||||
.input(RoleCreateArgsSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.roleService.create(input, staff);
|
||||
}),
|
||||
update: this.trpc.protectProcedure
|
||||
.input(RoleUpdateArgsSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.roleService.update(input, staff);
|
||||
}),
|
||||
createMany: this.trpc.protectProcedure.input(z.array(RoleCreateManyInputSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly roleService: RoleService,
|
||||
) {}
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.protectProcedure
|
||||
.input(RoleCreateArgsSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.roleService.create(input, staff);
|
||||
}),
|
||||
update: this.trpc.protectProcedure
|
||||
.input(RoleUpdateArgsSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.roleService.update(input, staff);
|
||||
}),
|
||||
createMany: this.trpc.protectProcedure
|
||||
.input(z.array(RoleCreateManyInputSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
|
||||
return await this.roleService.createMany({ data: input }, staff);
|
||||
}),
|
||||
softDeleteByIds: this.trpc.protectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
ids: z.array(z.string()),
|
||||
data: RoleUpdateInputSchema.optional()
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await this.roleService.softDeleteByIds(input.ids, input.data);
|
||||
}),
|
||||
findFirst: this.trpc.procedure
|
||||
.input(RoleFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.roleService.findFirst(input);
|
||||
}),
|
||||
return await this.roleService.createMany({ data: input }, staff);
|
||||
}),
|
||||
softDeleteByIds: this.trpc.protectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
ids: z.array(z.string()),
|
||||
data: RoleUpdateInputSchema.optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return await this.roleService.softDeleteByIds(input.ids, input.data);
|
||||
}),
|
||||
findFirst: this.trpc.procedure
|
||||
.input(RoleFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.roleService.findFirst(input);
|
||||
}),
|
||||
|
||||
updateOrder: this.trpc.protectProcedure
|
||||
.input(UpdateOrderSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.roleService.updateOrder(input);
|
||||
}),
|
||||
findMany: this.trpc.procedure
|
||||
.input(RoleFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.roleService.findMany(input);
|
||||
}),
|
||||
findManyWithCursor: this.trpc.protectProcedure
|
||||
.input(z.object({
|
||||
cursor: z.any().nullish(),
|
||||
take: z.number().optional(),
|
||||
where: RoleWhereInputSchema.optional(),
|
||||
select: RoleSelectSchema.optional()
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.roleService.findManyWithCursor(input);
|
||||
}),
|
||||
findManyWithPagination: this.trpc.procedure
|
||||
.input(z.object({
|
||||
page: z.number(),
|
||||
pageSize: z.number().optional(),
|
||||
where: RoleWhereInputSchema.optional(),
|
||||
select: RoleSelectSchema.optional()
|
||||
})) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.roleService.findManyWithPagination(input);
|
||||
}),
|
||||
});
|
||||
updateOrder: this.trpc.protectProcedure
|
||||
.input(UpdateOrderSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.roleService.updateOrder(input);
|
||||
}),
|
||||
findMany: this.trpc.procedure
|
||||
.input(RoleFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.roleService.findMany(input);
|
||||
}),
|
||||
findManyWithCursor: this.trpc.protectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
cursor: z.any().nullish(),
|
||||
take: z.number().optional(),
|
||||
where: RoleWhereInputSchema.optional(),
|
||||
select: RoleSelectSchema.optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.roleService.findManyWithCursor(input);
|
||||
}),
|
||||
findManyWithPagination: this.trpc.procedure
|
||||
.input(
|
||||
z.object({
|
||||
page: z.number(),
|
||||
pageSize: z.number().optional(),
|
||||
where: RoleWhereInputSchema.optional(),
|
||||
select: RoleSelectSchema.optional(),
|
||||
}),
|
||||
) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.roleService.findManyWithPagination(input);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,47 +1,59 @@
|
|||
import { db, ObjectType, RowModelRequest, RowRequestSchema, UserProfile } from "@nice/common";
|
||||
import { RowCacheService } from "../base/row-cache.service";
|
||||
import { isFieldCondition, LogicalCondition } from "../base/sql-builder";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
db,
|
||||
ObjectType,
|
||||
RowModelRequest,
|
||||
RowRequestSchema,
|
||||
UserProfile,
|
||||
} from '@nice/common';
|
||||
import { RowCacheService } from '../base/row-cache.service';
|
||||
import { isFieldCondition, LogicalCondition } from '../base/sql-builder';
|
||||
import { z } from 'zod';
|
||||
export class RoleRowService extends RowCacheService {
|
||||
protected createGetRowsFilters(
|
||||
request: z.infer<typeof RowRequestSchema>,
|
||||
staff?: UserProfile
|
||||
) {
|
||||
const condition = super.createGetRowsFilters(request)
|
||||
if (isFieldCondition(condition))
|
||||
return {}
|
||||
const baseModelCondition: LogicalCondition[] = [{
|
||||
field: `${this.tableName}.deleted_at`,
|
||||
op: "blank",
|
||||
type: "date"
|
||||
}]
|
||||
condition.AND = [...baseModelCondition, ...condition.AND!]
|
||||
return condition
|
||||
}
|
||||
createUnGroupingRowSelect(): string[] {
|
||||
return [
|
||||
`${this.tableName}.id AS id`,
|
||||
`${this.tableName}.name AS name`,
|
||||
`${this.tableName}.system AS system`,
|
||||
`${this.tableName}.permissions AS permissions`
|
||||
];
|
||||
}
|
||||
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
|
||||
if (!data.id)
|
||||
return data
|
||||
const roleMaps = await db.roleMap.findMany({
|
||||
where: {
|
||||
roleId: data.id
|
||||
}
|
||||
})
|
||||
const deptIds = roleMaps.filter(item => item.objectType === ObjectType.DEPARTMENT).map(roleMap => roleMap.objectId)
|
||||
const staffIds = roleMaps.filter(item => item.objectType === ObjectType.STAFF).map(roleMap => roleMap.objectId)
|
||||
const depts = await db.department.findMany({ where: { id: { in: deptIds } } })
|
||||
const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } })
|
||||
const result = { ...data, depts, staffs }
|
||||
return result
|
||||
}
|
||||
createJoinSql(request?: RowModelRequest): string[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
protected createGetRowsFilters(
|
||||
request: z.infer<typeof RowRequestSchema>,
|
||||
staff?: UserProfile,
|
||||
) {
|
||||
const condition = super.createGetRowsFilters(request);
|
||||
if (isFieldCondition(condition)) return {};
|
||||
const baseModelCondition: LogicalCondition[] = [
|
||||
{
|
||||
field: `${this.tableName}.deleted_at`,
|
||||
op: 'blank',
|
||||
type: 'date',
|
||||
},
|
||||
];
|
||||
condition.AND = [...baseModelCondition, ...condition.AND!];
|
||||
return condition;
|
||||
}
|
||||
createUnGroupingRowSelect(): string[] {
|
||||
return [
|
||||
`${this.tableName}.id AS id`,
|
||||
`${this.tableName}.name AS name`,
|
||||
`${this.tableName}.system AS system`,
|
||||
`${this.tableName}.permissions AS permissions`,
|
||||
];
|
||||
}
|
||||
protected async getRowDto(data: any, staff?: UserProfile): Promise<any> {
|
||||
if (!data.id) return data;
|
||||
const roleMaps = await db.roleMap.findMany({
|
||||
where: {
|
||||
roleId: data.id,
|
||||
},
|
||||
});
|
||||
const deptIds = roleMaps
|
||||
.filter((item) => item.objectType === ObjectType.DEPARTMENT)
|
||||
.map((roleMap) => roleMap.objectId);
|
||||
const staffIds = roleMaps
|
||||
.filter((item) => item.objectType === ObjectType.STAFF)
|
||||
.map((roleMap) => roleMap.objectId);
|
||||
const depts = await db.department.findMany({
|
||||
where: { id: { in: deptIds } },
|
||||
});
|
||||
const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } });
|
||||
const result = { ...data, depts, staffs };
|
||||
return result;
|
||||
}
|
||||
createJoinSql(request?: RowModelRequest): string[] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import {
|
||||
ObjectType,
|
||||
RoleMapMethodSchema,
|
||||
} from '@nice/common';
|
||||
import { ObjectType, RoleMapMethodSchema } from '@nice/common';
|
||||
import { RoleMapService } from './rolemap.service';
|
||||
|
||||
@Injectable()
|
||||
|
@ -11,7 +8,7 @@ export class RoleMapRouter {
|
|||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly roleMapService: RoleMapService,
|
||||
) { }
|
||||
) {}
|
||||
router = this.trpc.router({
|
||||
deleteAllRolesForObject: this.trpc.protectProcedure
|
||||
.input(RoleMapMethodSchema.deleteWithObject)
|
||||
|
|
|
@ -64,10 +64,7 @@ export class RoleMapService extends RowModelService {
|
|||
return condition;
|
||||
}
|
||||
|
||||
protected async getRowDto(
|
||||
row: any,
|
||||
staff?: UserProfile,
|
||||
): Promise<any> {
|
||||
protected async getRowDto(row: any, staff?: UserProfile): Promise<any> {
|
||||
if (!row.id) return row;
|
||||
return row;
|
||||
}
|
||||
|
@ -126,15 +123,17 @@ export class RoleMapService extends RowModelService {
|
|||
data: roleMaps,
|
||||
});
|
||||
});
|
||||
const wrapResult = Promise.all(result.map(async item => {
|
||||
const staff = await db.staff.findMany({
|
||||
include: { department: true },
|
||||
where: {
|
||||
id: item.objectId
|
||||
}
|
||||
})
|
||||
return { ...item, staff }
|
||||
}))
|
||||
const wrapResult = Promise.all(
|
||||
result.map(async (item) => {
|
||||
const staff = await db.staff.findMany({
|
||||
include: { department: true },
|
||||
where: {
|
||||
id: item.objectId,
|
||||
},
|
||||
});
|
||||
return { ...item, staff };
|
||||
}),
|
||||
);
|
||||
return wrapResult;
|
||||
}
|
||||
async addRoleForObjects(
|
||||
|
@ -187,11 +186,11 @@ export class RoleMapService extends RowModelService {
|
|||
{ objectId: staffId, objectType: ObjectType.STAFF },
|
||||
...(deptId || ancestorDeptIds.length > 0
|
||||
? [
|
||||
{
|
||||
objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) },
|
||||
objectType: ObjectType.DEPARTMENT,
|
||||
},
|
||||
]
|
||||
{
|
||||
objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) },
|
||||
objectType: ObjectType.DEPARTMENT,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
// Helper function to fetch roles based on domain ID.
|
||||
|
@ -260,7 +259,9 @@ export class RoleMapService extends RowModelService {
|
|||
// const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
|
||||
return { items, totalCount };
|
||||
}
|
||||
async getStaffsNotMap(data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>) {
|
||||
async getStaffsNotMap(
|
||||
data: z.infer<typeof RoleMapMethodSchema.getStaffsNotMap>,
|
||||
) {
|
||||
const { domainId, roleId } = data;
|
||||
let staffs = await db.staff.findMany({
|
||||
where: {
|
||||
|
@ -300,7 +301,9 @@ export class RoleMapService extends RowModelService {
|
|||
* @param data 包含角色ID和域ID的数据
|
||||
* @returns 角色映射详情,包含部门ID和员工ID列表
|
||||
*/
|
||||
async getRoleMapDetail(data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>) {
|
||||
async getRoleMapDetail(
|
||||
data: z.infer<typeof RoleMapMethodSchema.getRoleMapDetail>,
|
||||
) {
|
||||
const { roleId, domainId } = data;
|
||||
const res = await db.roleMap.findMany({ where: { roleId, domainId } });
|
||||
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import path, { dirname } from "path";
|
||||
import { FileMetadata, VideoMetadata, ResourceProcessor } from "../types";
|
||||
import { Resource, ResourceStatus, db } from "@nice/common";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import path, { dirname } from 'path';
|
||||
import { FileMetadata, VideoMetadata, ResourceProcessor } from '../types';
|
||||
import { Resource, ResourceStatus, db } from '@nice/common';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
export abstract class BaseProcessor implements ResourceProcessor {
|
||||
constructor() { }
|
||||
protected logger = new Logger(BaseProcessor.name)
|
||||
constructor() {}
|
||||
protected logger = new Logger(BaseProcessor.name);
|
||||
|
||||
abstract process(resource: Resource): Promise<Resource>
|
||||
protected createOutputDir(filepath: string, subdirectory: string = 'assets'): string {
|
||||
const outputDir = path.join(
|
||||
path.dirname(filepath),
|
||||
subdirectory,
|
||||
);
|
||||
fs.mkdir(outputDir, { recursive: true }).catch(err => this.logger.error(`Failed to create directory: ${err.message}`));
|
||||
|
||||
return outputDir;
|
||||
|
||||
}
|
||||
abstract process(resource: Resource): Promise<Resource>;
|
||||
protected createOutputDir(
|
||||
filepath: string,
|
||||
subdirectory: string = 'assets',
|
||||
): string {
|
||||
const outputDir = path.join(path.dirname(filepath), subdirectory);
|
||||
fs.mkdir(outputDir, { recursive: true }).catch((err) =>
|
||||
this.logger.error(`Failed to create directory: ${err.message}`),
|
||||
);
|
||||
|
||||
return outputDir;
|
||||
}
|
||||
}
|
||||
//
|
||||
//
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ResourceService } from './resource.service';
|
|||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
||||
@Module({
|
||||
exports: [ResourceRouter, ResourceService],
|
||||
providers: [ResourceRouter, ResourceService, TrpcService],
|
||||
exports: [ResourceRouter, ResourceService],
|
||||
providers: [ResourceRouter, ResourceService, TrpcService],
|
||||
})
|
||||
export class ResourceModule { }
|
||||
export class ResourceModule {}
|
||||
|
|
|
@ -1,55 +1,57 @@
|
|||
import { Resource } from "@nice/common";
|
||||
import { Resource } from '@nice/common';
|
||||
|
||||
export interface ResourceProcessor {
|
||||
process(resource: Resource): Promise<any>
|
||||
process(resource: Resource): Promise<any>;
|
||||
}
|
||||
export interface ProcessResult {
|
||||
success: boolean
|
||||
resource: Resource
|
||||
error?: Error
|
||||
success: boolean;
|
||||
resource: Resource;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export interface BaseMetadata {
|
||||
size: number
|
||||
filetype: string
|
||||
filename: string
|
||||
extension: string
|
||||
modifiedAt: Date
|
||||
size: number;
|
||||
filetype: string;
|
||||
filename: string;
|
||||
extension: string;
|
||||
modifiedAt: Date;
|
||||
}
|
||||
/**
|
||||
* 图片特有元数据接口
|
||||
*/
|
||||
export interface ImageMetadata {
|
||||
width: number; // 图片宽度(px)
|
||||
height: number; // 图片高度(px)
|
||||
compressedUrl?: string;
|
||||
orientation?: number; // EXIF方向信息
|
||||
space?: string; // 色彩空间 (如: RGB, CMYK)
|
||||
hasAlpha?: boolean; // 是否包含透明通道
|
||||
width: number; // 图片宽度(px)
|
||||
height: number; // 图片高度(px)
|
||||
compressedUrl?: string;
|
||||
orientation?: number; // EXIF方向信息
|
||||
space?: string; // 色彩空间 (如: RGB, CMYK)
|
||||
hasAlpha?: boolean; // 是否包含透明通道
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频特有元数据接口
|
||||
*/
|
||||
export interface VideoMetadata {
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
videoCodec?: string;
|
||||
audioCodec?: string;
|
||||
coverUrl?: string
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
videoCodec?: string;
|
||||
audioCodec?: string;
|
||||
coverUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 音频特有元数据接口
|
||||
*/
|
||||
export interface AudioMetadata {
|
||||
duration: number; // 音频时长(秒)
|
||||
bitrate?: number; // 比特率(bps)
|
||||
sampleRate?: number; // 采样率(Hz)
|
||||
channels?: number; // 声道数
|
||||
codec?: string; // 音频编码格式
|
||||
duration: number; // 音频时长(秒)
|
||||
bitrate?: number; // 比特率(bps)
|
||||
sampleRate?: number; // 采样率(Hz)
|
||||
channels?: number; // 声道数
|
||||
codec?: string; // 音频编码格式
|
||||
}
|
||||
|
||||
|
||||
export type FileMetadata = ImageMetadata & VideoMetadata & AudioMetadata & BaseMetadata
|
||||
export type FileMetadata = ImageMetadata &
|
||||
VideoMetadata &
|
||||
AudioMetadata &
|
||||
BaseMetadata;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Controller, UseGuards } from "@nestjs/common";
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@server/auth/auth.guard';
|
||||
import { sportProjectService } from "./sportProject.service";
|
||||
import { sportProjectService } from './sportProject.service';
|
||||
|
||||
@Controller('sportProject')
|
||||
export class sportProjectController {
|
||||
constructor(private readonly sportProjectService: sportProjectService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
constructor(private readonly sportProjectService: sportProjectService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { SportProjectRouter } from './sportProject.router';
|
|||
@Module({
|
||||
imports: [StaffModule],
|
||||
controllers: [sportProjectController],
|
||||
providers: [sportProjectService,SportProjectRouter,TrpcService],
|
||||
exports: [sportProjectService,SportProjectRouter],
|
||||
providers: [sportProjectService, SportProjectRouter, TrpcService],
|
||||
exports: [sportProjectService, SportProjectRouter],
|
||||
})
|
||||
export class SportProjectModule {}
|
||||
export class SportProjectModule {}
|
||||
|
|
|
@ -1,42 +1,49 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { TrpcService } from "@server/trpc/trpc.service";
|
||||
import { sportProjectService } from "./sportProject.service";
|
||||
import { z, ZodType } from "zod";
|
||||
import { Prisma } from "@nice/common";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { sportProjectService } from './sportProject.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
import { Prisma } from '@nice/common';
|
||||
|
||||
const SportProjectArgsSchema:ZodType<Prisma.SportProjectCreateArgs> = z.any()
|
||||
const SportProjectUpdateArgsSchema:ZodType<Prisma.SportProjectUpdateArgs> = z.any()
|
||||
const SportProjectFindManyArgsSchema:ZodType<Prisma.SportProjectFindManyArgs> = z.any()
|
||||
const SportProjectFindFirstArgsSchema:ZodType<Prisma.SportProjectFindFirstArgs> = z.any()
|
||||
const SportProjectArgsSchema: ZodType<Prisma.SportProjectCreateArgs> = z.any();
|
||||
const SportProjectUpdateArgsSchema: ZodType<Prisma.SportProjectUpdateArgs> =
|
||||
z.any();
|
||||
const SportProjectFindManyArgsSchema: ZodType<Prisma.SportProjectFindManyArgs> =
|
||||
z.any();
|
||||
const SportProjectFindFirstArgsSchema: ZodType<Prisma.SportProjectFindFirstArgs> =
|
||||
z.any();
|
||||
@Injectable()
|
||||
export class SportProjectRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly sportProjectService: sportProjectService,
|
||||
) { }
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly sportProjectService: sportProjectService,
|
||||
) {}
|
||||
|
||||
router = this.trpc.router({
|
||||
create:this.trpc.procedure.input(SportProjectArgsSchema)
|
||||
.mutation(async ({input})=>{
|
||||
console.log(input)
|
||||
return this.sportProjectService.create(input)
|
||||
}),
|
||||
update:this.trpc.procedure.input(SportProjectUpdateArgsSchema)
|
||||
.mutation(async ({input})=>{
|
||||
return this.sportProjectService.update(input)
|
||||
}),
|
||||
findMany:this.trpc.procedure.input(SportProjectFindManyArgsSchema)
|
||||
.query(async ({input})=>{
|
||||
return this.sportProjectService.findMany(input)
|
||||
}),
|
||||
softDeleteByIds:this.trpc.procedure.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({input})=>{
|
||||
return this.sportProjectService.softDeleteByIds(input.ids)
|
||||
}),
|
||||
findFirst:this.trpc.procedure.input(SportProjectFindFirstArgsSchema)
|
||||
.query(async ({input})=>{
|
||||
return this.sportProjectService.findFirst(input)
|
||||
}),
|
||||
})
|
||||
|
||||
}
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.procedure
|
||||
.input(SportProjectArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
console.log(input);
|
||||
return this.sportProjectService.create(input);
|
||||
}),
|
||||
update: this.trpc.procedure
|
||||
.input(SportProjectUpdateArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.sportProjectService.update(input);
|
||||
}),
|
||||
findMany: this.trpc.procedure
|
||||
.input(SportProjectFindManyArgsSchema)
|
||||
.query(async ({ input }) => {
|
||||
return this.sportProjectService.findMany(input);
|
||||
}),
|
||||
softDeleteByIds: this.trpc.procedure
|
||||
.input(z.object({ ids: z.array(z.string()) }))
|
||||
.mutation(async ({ input }) => {
|
||||
return this.sportProjectService.softDeleteByIds(input.ids);
|
||||
}),
|
||||
findFirst: this.trpc.procedure
|
||||
.input(SportProjectFindFirstArgsSchema)
|
||||
.query(async ({ input }) => {
|
||||
return this.sportProjectService.findFirst(input);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,59 +1,58 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseService } from "../base/base.service";
|
||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||
|
||||
interface AgeRange {
|
||||
start: number | null;
|
||||
end: number | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
start: number | null;
|
||||
end: number | null;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ScoreStandard {
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: {
|
||||
[score: string]: number[];
|
||||
}
|
||||
}
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: {
|
||||
[score: string]: number[];
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class sportProjectService extends BaseService<Prisma.SportProjectDelegate> {
|
||||
constructor() {
|
||||
super(db,ObjectType.SPORT_PROJECT,false);
|
||||
}
|
||||
async create(args: Prisma.SportProjectCreateArgs) {
|
||||
const result = await super.create(args)
|
||||
this.emitDataChanged(CrudOperation.CREATED,result)
|
||||
return result
|
||||
}
|
||||
constructor() {
|
||||
super(db, ObjectType.SPORT_PROJECT, false);
|
||||
}
|
||||
async create(args: Prisma.SportProjectCreateArgs) {
|
||||
const result = await super.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(args:Prisma.SportProjectUpdateArgs){
|
||||
const result = await super.update(args)
|
||||
this.emitDataChanged(CrudOperation.UPDATED,result)
|
||||
return result
|
||||
}
|
||||
async update(args: Prisma.SportProjectUpdateArgs) {
|
||||
const result = await super.update(args);
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.SportProjectFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
async findFirst(args: Prisma.SportProjectFindFirstArgs) {
|
||||
const result = await super.findFirst(args);
|
||||
return result;
|
||||
}
|
||||
async softDeleteByIds(ids: string[]) {
|
||||
const result = await super.softDeleteByIds(ids);
|
||||
this.emitDataChanged(CrudOperation.DELETED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.SportProjectFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
async findFirst(args: Prisma.SportProjectFindFirstArgs) {
|
||||
const result = await super.findFirst(args)
|
||||
return result
|
||||
}
|
||||
async softDeleteByIds(ids: string[]) {
|
||||
const result = await super.softDeleteByIds(ids);
|
||||
this.emitDataChanged(CrudOperation.DELETED,result)
|
||||
return result
|
||||
}
|
||||
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type:ObjectType.SPORT_STANDARD,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.SPORT_STANDARD,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Controller, UseGuards } from "@nestjs/common";
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@server/auth/auth.guard';
|
||||
import { SportStandardService } from "./sportStandard.service";
|
||||
import { SportStandardService } from './sportStandard.service';
|
||||
|
||||
@Controller('sportStandard')
|
||||
export class SportStandardController {
|
||||
constructor(private readonly sportStandardService: SportStandardService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
constructor(private readonly sportStandardService: SportStandardService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { SportStandardRouter } from './sportStandard.router';
|
|||
@Module({
|
||||
imports: [StaffModule],
|
||||
controllers: [SportStandardController],
|
||||
providers: [SportStandardService,SportStandardRouter,TrpcService],
|
||||
exports: [SportStandardService,SportStandardRouter],
|
||||
providers: [SportStandardService, SportStandardRouter, TrpcService],
|
||||
exports: [SportStandardService, SportStandardRouter],
|
||||
})
|
||||
export class SportStandardModule {}
|
||||
export class SportStandardModule {}
|
||||
|
|
|
@ -1,78 +1,92 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { TrpcService } from "@server/trpc/trpc.service";
|
||||
import { SportStandardService } from "./sportStandard.service";
|
||||
import { z, ZodType } from "zod";
|
||||
import { Prisma } from "@nice/common";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { SportStandardService } from './sportStandard.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
import { Prisma } from '@nice/common';
|
||||
|
||||
const SportStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> = z.any()
|
||||
const SportStandardUpdateArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> = z.any()
|
||||
const SportStandardFindManyArgsSchema: ZodType<Prisma.SportStandardFindManyArgs> = z.any()
|
||||
const SportStandardCreateStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> = z.any()
|
||||
const SportStandardUpdateStandardArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> = z.any()
|
||||
const SportStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> =
|
||||
z.any();
|
||||
const SportStandardUpdateArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> =
|
||||
z.any();
|
||||
const SportStandardFindManyArgsSchema: ZodType<Prisma.SportStandardFindManyArgs> =
|
||||
z.any();
|
||||
const SportStandardCreateStandardArgsSchema: ZodType<Prisma.SportStandardCreateArgs> =
|
||||
z.any();
|
||||
const SportStandardUpdateStandardArgsSchema: ZodType<Prisma.SportStandardUpdateArgs> =
|
||||
z.any();
|
||||
const GetScoreArgsSchema = z.object({
|
||||
projectId: z.string().nonempty(),
|
||||
gender: z.boolean(),
|
||||
age: z.number().min(0),
|
||||
performance: z.number().min(0).or(z.string()),
|
||||
personType: z.string().nonempty(),
|
||||
projectUnit: z.string().nonempty()
|
||||
projectId: z.string().nonempty(),
|
||||
gender: z.boolean(),
|
||||
age: z.number().min(0),
|
||||
performance: z.number().min(0).or(z.string()),
|
||||
personType: z.string().nonempty(),
|
||||
projectUnit: z.string().nonempty(),
|
||||
});
|
||||
|
||||
interface AgeRange {
|
||||
start: number | null;
|
||||
end: number | null;
|
||||
label: string;
|
||||
start: number | null;
|
||||
end: number | null;
|
||||
label: string;
|
||||
}
|
||||
interface Record {
|
||||
[key: number]: number[];
|
||||
[key: number]: number[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SportStandardRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly sportStandardService: SportStandardService,
|
||||
) { }
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly sportStandardService: SportStandardService,
|
||||
) {}
|
||||
|
||||
router = this.trpc.router({
|
||||
// create:this.trpc.procedure.input(SportStandardArgsSchema)
|
||||
// .mutation(async ({input})=>{
|
||||
// return this.sportStandardService.create(input)
|
||||
// }),
|
||||
update: this.trpc.procedure.input(SportStandardUpdateArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.sportStandardService.update(input)
|
||||
}),
|
||||
findMany: this.trpc.procedure.input(SportStandardFindManyArgsSchema)
|
||||
.query(async ({ input }) => {
|
||||
return this.sportStandardService.findMany(input)
|
||||
}),
|
||||
createStandard: this.trpc.procedure.input(SportStandardCreateStandardArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const data = {
|
||||
projectId: input.data.projectId,
|
||||
gender: input.data.gender,
|
||||
personType: input.data.personType,
|
||||
ageRanges: input.data.ageRanges as any as AgeRange[],
|
||||
scoreTable: input.data.scoreTable as Record
|
||||
}
|
||||
return this.sportStandardService.createStandard(data, input.select, input.include)
|
||||
}),
|
||||
updateStandard: this.trpc.procedure.input(SportStandardUpdateStandardArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const data = {
|
||||
id: input.data.id as string,
|
||||
ageRanges: input.data.ageRanges as any as AgeRange[],
|
||||
scoreTable: input.data.scoreTable as Record
|
||||
}
|
||||
return this.sportStandardService.updateStandard(data)
|
||||
}),
|
||||
router = this.trpc.router({
|
||||
// create:this.trpc.procedure.input(SportStandardArgsSchema)
|
||||
// .mutation(async ({input})=>{
|
||||
// return this.sportStandardService.create(input)
|
||||
// }),
|
||||
update: this.trpc.procedure
|
||||
.input(SportStandardUpdateArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.sportStandardService.update(input);
|
||||
}),
|
||||
findMany: this.trpc.procedure
|
||||
.input(SportStandardFindManyArgsSchema)
|
||||
.query(async ({ input }) => {
|
||||
return this.sportStandardService.findMany(input);
|
||||
}),
|
||||
createStandard: this.trpc.procedure
|
||||
.input(SportStandardCreateStandardArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const data = {
|
||||
projectId: input.data.projectId,
|
||||
gender: input.data.gender,
|
||||
personType: input.data.personType,
|
||||
ageRanges: input.data.ageRanges as any as AgeRange[],
|
||||
scoreTable: input.data.scoreTable as Record,
|
||||
};
|
||||
return this.sportStandardService.createStandard(
|
||||
data,
|
||||
input.select,
|
||||
input.include,
|
||||
);
|
||||
}),
|
||||
updateStandard: this.trpc.procedure
|
||||
.input(SportStandardUpdateStandardArgsSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
const data = {
|
||||
id: input.data.id as string,
|
||||
ageRanges: input.data.ageRanges as any as AgeRange[],
|
||||
scoreTable: input.data.scoreTable as Record,
|
||||
};
|
||||
return this.sportStandardService.updateStandard(data);
|
||||
}),
|
||||
|
||||
getScore: this.trpc.procedure.input(GetScoreArgsSchema).query(async ({ input }) => {
|
||||
console.log('计算')
|
||||
console.log(input)
|
||||
return this.sportStandardService.getScore(input);
|
||||
}),
|
||||
})
|
||||
|
||||
}
|
||||
getScore: this.trpc.procedure
|
||||
.input(GetScoreArgsSchema)
|
||||
.query(async ({ input }) => {
|
||||
console.log('计算');
|
||||
console.log(input);
|
||||
return this.sportStandardService.getScore(input);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,217 +1,230 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseService } from "../base/base.service";
|
||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import { z } from "zod";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||
import { z } from 'zod';
|
||||
|
||||
interface AgeRange {
|
||||
start: number | null;
|
||||
end: number | null;
|
||||
label: string;
|
||||
start: number | null;
|
||||
end: number | null;
|
||||
label: string;
|
||||
}
|
||||
interface Record {
|
||||
[key: number]: (number | string)[];
|
||||
[key: number]: (number | string)[];
|
||||
}
|
||||
interface ScoreStandard {
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: Record;
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: Record;
|
||||
}
|
||||
|
||||
const GetScoreArgsSchema = z.object({
|
||||
projectId: z.string().nonempty(),
|
||||
gender: z.boolean(),
|
||||
age: z.number().min(0),
|
||||
performance: z.number().min(0).or(z.string()),
|
||||
personType: z.string().nonempty(),
|
||||
projectUnit: z.string().nonempty()
|
||||
projectId: z.string().nonempty(),
|
||||
gender: z.boolean(),
|
||||
age: z.number().min(0),
|
||||
performance: z.number().min(0).or(z.string()),
|
||||
personType: z.string().nonempty(),
|
||||
projectUnit: z.string().nonempty(),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class SportStandardService extends BaseService<Prisma.SportStandardDelegate> {
|
||||
constructor() {
|
||||
super(db, ObjectType.SPORT_STANDARD, false);
|
||||
constructor() {
|
||||
super(db, ObjectType.SPORT_STANDARD, false);
|
||||
}
|
||||
async create(args: Prisma.SportStandardCreateArgs) {
|
||||
console.log(args);
|
||||
const result = await super.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(args: Prisma.SportStandardUpdateArgs) {
|
||||
const result = await super.update(args);
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.SportStandardFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
async findUnique(args: Prisma.SportStandardFindUniqueArgs) {
|
||||
const result = await super.findUnique(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.SPORT_STANDARD,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async createStandard(
|
||||
data: {
|
||||
projectId: string;
|
||||
gender: boolean;
|
||||
personType: string;
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: Record;
|
||||
},
|
||||
select?: Prisma.SportStandardSelect<DefaultArgs>,
|
||||
include?: Prisma.SportStandardInclude<DefaultArgs>,
|
||||
) {
|
||||
console.log(data);
|
||||
this.validateAgeRanges(data.ageRanges);
|
||||
this.validateScoreTable(data.scoreTable, data.ageRanges.length);
|
||||
const result = await super.create({
|
||||
data: {
|
||||
projectId: data.projectId,
|
||||
gender: data.gender,
|
||||
personType: data.personType,
|
||||
ageRanges: JSON.stringify(data.ageRanges),
|
||||
scoreTable: JSON.stringify(data.scoreTable),
|
||||
},
|
||||
select,
|
||||
include,
|
||||
});
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
private validateAgeRanges(ranges: AgeRange[]) {
|
||||
// 先按起始年龄排序
|
||||
const sortedRanges = [...ranges].sort(
|
||||
(a, b) => (a.start || 0) - (b.start || 0),
|
||||
);
|
||||
|
||||
for (let i = 0; i < sortedRanges.length - 1; i++) {
|
||||
const current = sortedRanges[i];
|
||||
const next = sortedRanges[i + 1];
|
||||
// 检查重叠
|
||||
if ((current.end || Infinity) >= next.start) {
|
||||
throw new Error(`年龄范围 ${current.label} 和 ${next.label} 重叠`);
|
||||
}
|
||||
// 检查连续性(允许有间隔)
|
||||
if ((current.end || Infinity) + 1 > next.start) {
|
||||
throw new Error(`年龄范围 ${current.label} 和 ${next.label} 不连续`);
|
||||
}
|
||||
}
|
||||
async create(args: Prisma.SportStandardCreateArgs) {
|
||||
console.log(args)
|
||||
const result = await super.create(args)
|
||||
this.emitDataChanged(CrudOperation.CREATED, result)
|
||||
return result
|
||||
}
|
||||
|
||||
private validateScoreTable(scoreTable: Record, expectedLength: number) {
|
||||
Object.values(scoreTable).forEach((standards) => {
|
||||
if (standards.length !== expectedLength) {
|
||||
throw new Error('分数表的每行数据长度必须与年龄段数量匹配');
|
||||
}
|
||||
});
|
||||
}
|
||||
async updateStandard(data: {
|
||||
id: string;
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: Record;
|
||||
}) {
|
||||
this.validateAgeRanges(data.ageRanges);
|
||||
this.validateScoreTable(data.scoreTable, data.ageRanges.length);
|
||||
const result = await super.update({
|
||||
where: {
|
||||
id: data.id,
|
||||
},
|
||||
data: {
|
||||
ageRanges: JSON.stringify(data.ageRanges),
|
||||
scoreTable: JSON.stringify(data.scoreTable),
|
||||
},
|
||||
});
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public SportScoreCalculator(
|
||||
performance: number | string,
|
||||
age: number,
|
||||
scoreStandard: ScoreStandard,
|
||||
projectUnit: string,
|
||||
): number {
|
||||
// 1. 找到对应的年龄段索引
|
||||
const ageRangeIndex = scoreStandard.ageRanges.findIndex((range) => {
|
||||
const isAboveStart = range.start === null || age > range.start;
|
||||
const isBelowEnd = range.end === null || age <= range.end;
|
||||
return isAboveStart && isBelowEnd;
|
||||
});
|
||||
|
||||
if (ageRangeIndex === -1) {
|
||||
throw new Error('未找到匹配的年龄段');
|
||||
}
|
||||
|
||||
async update(args: Prisma.SportStandardUpdateArgs) {
|
||||
const result = await super.update(args)
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result)
|
||||
return result
|
||||
// 2. 查找对应分数
|
||||
const scores = Object.keys(scoreStandard.scoreTable)
|
||||
.map(Number)
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
const isTimeUnit = projectUnit.includes('time'); // 假设时间单位包含 '时间单位' 字符串
|
||||
|
||||
for (const score of scores) {
|
||||
const standard =
|
||||
scoreStandard.scoreTable[score.toString()][ageRangeIndex];
|
||||
if (isTimeUnit) {
|
||||
// 此时的performance和standard是时间字符串
|
||||
// 需要将时间字符串转换为秒
|
||||
if (
|
||||
this.timeStringToSeconds(performance) <=
|
||||
this.timeStringToSeconds(standard)
|
||||
) {
|
||||
return score;
|
||||
}
|
||||
} else {
|
||||
if (performance >= standard) {
|
||||
return score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.SportStandardFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
async findUnique(args: Prisma.SportStandardFindUniqueArgs) {
|
||||
const result = await super.findUnique(args)
|
||||
return result
|
||||
}
|
||||
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.SPORT_STANDARD,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async createStandard(
|
||||
data: {
|
||||
projectId: string;
|
||||
gender: boolean;
|
||||
personType: string;
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: Record;
|
||||
async getScore(data: z.infer<typeof GetScoreArgsSchema>) {
|
||||
console.log('传入的参数', data);
|
||||
const standard = await this.findUnique({
|
||||
where: {
|
||||
projectId_gender_personType: {
|
||||
// 使用复合唯一索引
|
||||
projectId: data.projectId,
|
||||
gender: data.gender,
|
||||
personType: data.personType,
|
||||
},
|
||||
select?: Prisma.SportStandardSelect<DefaultArgs>,
|
||||
include?: Prisma.SportStandardInclude<DefaultArgs>
|
||||
) {
|
||||
console.log(data)
|
||||
this.validateAgeRanges(data.ageRanges);
|
||||
this.validateScoreTable(data.scoreTable, data.ageRanges.length);
|
||||
const result = await super.create({
|
||||
data: {
|
||||
projectId: data.projectId,
|
||||
gender: data.gender,
|
||||
personType: data.personType,
|
||||
ageRanges: JSON.stringify(data.ageRanges),
|
||||
scoreTable: JSON.stringify(data.scoreTable)
|
||||
},
|
||||
select,
|
||||
include
|
||||
})
|
||||
this.emitDataChanged(CrudOperation.CREATED, result)
|
||||
return result
|
||||
},
|
||||
});
|
||||
console.log('找到的评分标准', standard);
|
||||
if (!standard) {
|
||||
throw new Error('未找到对应的评分标准');
|
||||
}
|
||||
private validateAgeRanges(ranges: AgeRange[]) {
|
||||
// 检查年龄段是否按顺序排列且无重叠
|
||||
for (let i = 0; i < ranges.length - 1; i++) {
|
||||
const current = ranges[i];
|
||||
const next = ranges[i + 1];
|
||||
const scoreTable: Record = JSON.parse(String(standard.scoreTable));
|
||||
const ageRanges: AgeRange[] = JSON.parse(String(standard.ageRanges));
|
||||
const scoreStandard: ScoreStandard = {
|
||||
ageRanges,
|
||||
scoreTable,
|
||||
};
|
||||
console.log('评分标准', scoreStandard);
|
||||
return this.SportScoreCalculator(
|
||||
data.performance,
|
||||
data.age,
|
||||
scoreStandard,
|
||||
data.projectUnit,
|
||||
);
|
||||
}
|
||||
// 将 13:45 格式的字符串转换为秒数
|
||||
private timeStringToSeconds(timeStr) {
|
||||
const [minutes, seconds] = timeStr.split(':').map(Number);
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
if (current.end !== next.start) {
|
||||
throw new Error('年龄段必须连续且不重叠');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateScoreTable(
|
||||
scoreTable: Record,
|
||||
expectedLength: number
|
||||
) {
|
||||
Object.values(scoreTable).forEach(standards => {
|
||||
if (standards.length !== expectedLength) {
|
||||
throw new Error('分数表的每行数据长度必须与年龄段数量匹配');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async updateStandard(
|
||||
data: {
|
||||
id: string;
|
||||
ageRanges: AgeRange[];
|
||||
scoreTable: Record;
|
||||
}
|
||||
) {
|
||||
this.validateAgeRanges(data.ageRanges);
|
||||
this.validateScoreTable(data.scoreTable, data.ageRanges.length);
|
||||
const result = await super.update({
|
||||
where: {
|
||||
id: data.id
|
||||
},
|
||||
data: {
|
||||
ageRanges: JSON.stringify(data.ageRanges),
|
||||
scoreTable: JSON.stringify(data.scoreTable)
|
||||
}
|
||||
})
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result)
|
||||
return result
|
||||
}
|
||||
|
||||
public SportScoreCalculator(performance: number | string, age: number, scoreStandard: ScoreStandard, projectUnit: string, ): number {
|
||||
// 1. 找到对应的年龄段索引
|
||||
const ageRangeIndex = scoreStandard.ageRanges.findIndex(range => {
|
||||
const isAboveStart = range.start === null || age > range.start;
|
||||
const isBelowEnd = range.end === null || age <= range.end;
|
||||
return isAboveStart && isBelowEnd;
|
||||
});
|
||||
|
||||
if (ageRangeIndex === -1) {
|
||||
throw new Error('未找到匹配的年龄段');
|
||||
}
|
||||
|
||||
// 2. 查找对应分数
|
||||
const scores = Object.keys(scoreStandard.scoreTable)
|
||||
.map(Number)
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
const isTimeUnit = projectUnit.includes('time'); // 假设时间单位包含 '时间单位' 字符串
|
||||
|
||||
for (const score of scores) {
|
||||
const standard = scoreStandard.scoreTable[score.toString()][ageRangeIndex];
|
||||
if (isTimeUnit) {
|
||||
// 此时的performance和standard是时间字符串
|
||||
// 需要将时间字符串转换为秒
|
||||
if (this.timeStringToSeconds(performance) <= this.timeStringToSeconds(standard)) {
|
||||
return score;
|
||||
}
|
||||
} else {
|
||||
if (performance >= standard) {
|
||||
return score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async getScore(data: z.infer<typeof GetScoreArgsSchema>) {
|
||||
console.log("传入的参数",data)
|
||||
const standard = await this.findUnique({
|
||||
where: {
|
||||
projectId_gender_personType: { // 使用复合唯一索引
|
||||
projectId: data.projectId,
|
||||
gender: data.gender,
|
||||
personType: data.personType
|
||||
}
|
||||
}
|
||||
})
|
||||
console.log("找到的评分标准",standard)
|
||||
if (!standard) {
|
||||
throw new Error('未找到对应的评分标准');
|
||||
}
|
||||
const scoreTable:Record = JSON.parse(String(standard.scoreTable))
|
||||
const ageRanges:AgeRange[] = JSON.parse(String(standard.ageRanges))
|
||||
const scoreStandard:ScoreStandard = {
|
||||
ageRanges,
|
||||
scoreTable
|
||||
}
|
||||
console.log("评分标准",scoreStandard)
|
||||
return this.SportScoreCalculator(
|
||||
data.performance,
|
||||
data.age,
|
||||
scoreStandard,
|
||||
data.projectUnit,
|
||||
)
|
||||
}
|
||||
// 将 13:45 格式的字符串转换为秒数
|
||||
private timeStringToSeconds(timeStr) {
|
||||
const [minutes, seconds] = timeStr.split(':').map(Number);
|
||||
return minutes * 60 + seconds;
|
||||
}
|
||||
|
||||
// 将秒数转换为 13:45 格式的字符串
|
||||
private secondsToTimeString(seconds: number) {
|
||||
const minutesPart = Math.floor(seconds / 60);
|
||||
const secondsPart = seconds % 60;
|
||||
return `${String(minutesPart).padStart(2, '0')}:${String(secondsPart).padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
// 将秒数转换为 13:45 格式的字符串
|
||||
private secondsToTimeString(seconds: number) {
|
||||
const minutesPart = Math.floor(seconds / 60);
|
||||
const secondsPart = seconds % 60;
|
||||
return `${String(minutesPart).padStart(2, '0')}:${String(
|
||||
secondsPart,
|
||||
).padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,14 @@ export class StaffController {
|
|||
}
|
||||
@Get('find-by-dept')
|
||||
async findByDept(
|
||||
@Query('dept-id') deptId: string,
|
||||
@Query('dept-id') deptId: string | null,
|
||||
@Query('domain-id') domainId: string,
|
||||
) {
|
||||
try {
|
||||
const result = await this.staffService.findByDept({ deptId, domainId });
|
||||
const result = await this.staffService.findByDept({
|
||||
deptId: deptId || null,
|
||||
domainId: domainId,
|
||||
});
|
||||
return {
|
||||
data: result,
|
||||
errmsg: 'success',
|
||||
|
|
|
@ -12,4 +12,4 @@ import { StaffRowService } from './staff.row.service';
|
|||
exports: [StaffService, StaffRouter, StaffRowService],
|
||||
controllers: [StaffController],
|
||||
})
|
||||
export class StaffModule { }
|
||||
export class StaffModule {}
|
||||
|
|
|
@ -95,7 +95,7 @@ export class StaffRouter {
|
|||
.query(async ({ input }) => {
|
||||
return await this.staffService.findUnique(input);
|
||||
}),
|
||||
findSportStaffByDept:this.trpc.procedure
|
||||
findSportStaffByDept: this.trpc.procedure
|
||||
.input(StaffMethodSchema.findSportStaffByDept)
|
||||
.query(async ({ input }) => {
|
||||
return await this.staffService.findSportStaffByDept(input);
|
||||
|
|
|
@ -37,8 +37,8 @@ const StaffSelect = {
|
|||
type: true,
|
||||
categorize: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
// 获取关联的 TrainSituation 模型的基础信息
|
||||
trainSituations: {
|
||||
|
@ -47,7 +47,7 @@ const StaffSelect = {
|
|||
score: true,
|
||||
value: true,
|
||||
alreadyTrainTime: true,
|
||||
groupId:true,
|
||||
groupId: true,
|
||||
// 获取关联的 TrainContent 模型的基础信息
|
||||
trainContent: {
|
||||
select: {
|
||||
|
@ -57,21 +57,23 @@ const StaffSelect = {
|
|||
parentId: true,
|
||||
deletedAt: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
}
|
||||
}
|
||||
}
|
||||
updatedAt: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
department:{
|
||||
select:{
|
||||
id:true,
|
||||
department: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
||||
//索引
|
||||
[x: string]: any;
|
||||
constructor(private readonly departmentService: DepartmentService) {
|
||||
super(db, ObjectType.STAFF, true);
|
||||
}
|
||||
|
@ -83,7 +85,7 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
|||
async findByDept(data: z.infer<typeof StaffMethodSchema.findByDept>) {
|
||||
const { deptId, domainId } = data;
|
||||
const childDepts = await this.departmentService.getDescendantIds(
|
||||
deptId,
|
||||
deptId || null,
|
||||
true,
|
||||
);
|
||||
const result = await db.staff.findMany({
|
||||
|
@ -176,30 +178,35 @@ export class StaffService extends BaseService<Prisma.StaffDelegate> {
|
|||
return staff;
|
||||
}
|
||||
}
|
||||
async findSportStaffByDept(data: z.infer<typeof StaffMethodSchema.findSportStaffByDept>) {
|
||||
async findSportStaffByDept(
|
||||
data: z.infer<typeof StaffMethodSchema.findSportStaffByDept>,
|
||||
) {
|
||||
const { deptId, domainId } = data;
|
||||
let queryResult;
|
||||
|
||||
if (!deptId) {
|
||||
// deptId 为空时执行 res 的请求
|
||||
queryResult = await db.staff.findMany({
|
||||
select: StaffSelect
|
||||
select: StaffSelect,
|
||||
});
|
||||
} else {
|
||||
// deptId 不为空时执行 result 的请求
|
||||
const childDepts = await this.departmentService.getDescendantIds(deptId, true);
|
||||
const childDepts = await this.departmentService.getDescendantIds(
|
||||
deptId,
|
||||
true,
|
||||
);
|
||||
queryResult = await db.staff.findMany({
|
||||
where: {
|
||||
deptId: { in: childDepts },
|
||||
domainId,
|
||||
},
|
||||
select: StaffSelect
|
||||
select: StaffSelect,
|
||||
});
|
||||
}
|
||||
|
||||
// 筛选出 trainSituations 中 trainContent 的 type 为 'SPORT' 的记录
|
||||
const filteredResult = queryResult.filter(staff => {
|
||||
return staff.trainSituations.some(trainSituation => {
|
||||
const filteredResult = queryResult.filter((staff) => {
|
||||
return staff.trainSituations.some((trainSituation) => {
|
||||
return trainSituation.trainContent.type === 'SPORT';
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ export class TaxonomyRouter {
|
|||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly taxonomyService: TaxonomyService,
|
||||
) { }
|
||||
) {}
|
||||
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.procedure
|
||||
|
|
|
@ -13,4 +13,4 @@ import { TermRowService } from './term.row.service';
|
|||
exports: [TermService, TermRouter],
|
||||
controllers: [TermController],
|
||||
})
|
||||
export class TermModule { }
|
||||
export class TermModule {}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Controller, UseGuards } from "@nestjs/common";
|
||||
import { TrainContentService } from "./trainContent.service";
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { TrainContentService } from './trainContent.service';
|
||||
import { AuthGuard } from '@server/auth/auth.guard';
|
||||
|
||||
@Controller('train-content')
|
||||
export class TrainContentController {
|
||||
constructor(private readonly trainContentService: TrainContentService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
constructor(private readonly trainContentService: TrainContentService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
|
|
|
@ -5,11 +5,10 @@ import { TrainContentController } from './trainContent.controller';
|
|||
import { TrainContentRouter } from './trainContent.router';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [StaffModule],
|
||||
controllers: [TrainContentController],
|
||||
providers: [TrainContentService,TrainContentRouter,TrpcService],
|
||||
exports: [TrainContentService,TrainContentRouter],
|
||||
providers: [TrainContentService, TrainContentRouter, TrpcService],
|
||||
exports: [TrainContentService, TrainContentRouter],
|
||||
})
|
||||
export class TrainContentModule {}
|
||||
export class TrainContentModule {}
|
||||
|
|
|
@ -1,37 +1,43 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { TrpcService } from "@server/trpc/trpc.service";
|
||||
import { TrainContentService } from "./trainContent.service";
|
||||
import { z, ZodType } from "zod";
|
||||
import { Prisma } from "@nice/common";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { TrainContentService } from './trainContent.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
import { Prisma } from '@nice/common';
|
||||
|
||||
const TrainContentArgsSchema:ZodType<Prisma.TrainContentCreateArgs> = z.any()
|
||||
const TrainContentUpdateArgsSchema:ZodType<Prisma.TrainContentUpdateArgs> = z.any()
|
||||
const TrainContentFindManyArgsSchema:ZodType<Prisma.TrainContentFindManyArgs> = z.any()
|
||||
const TrainContentFindFirstArgsSchema:ZodType<Prisma.TrainContentFindFirstArgs> = z.any()
|
||||
const TrainContentArgsSchema: ZodType<Prisma.TrainContentCreateArgs> = z.any();
|
||||
const TrainContentUpdateArgsSchema: ZodType<Prisma.TrainContentUpdateArgs> =
|
||||
z.any();
|
||||
const TrainContentFindManyArgsSchema: ZodType<Prisma.TrainContentFindManyArgs> =
|
||||
z.any();
|
||||
const TrainContentFindFirstArgsSchema: ZodType<Prisma.TrainContentFindFirstArgs> =
|
||||
z.any();
|
||||
@Injectable()
|
||||
export class TrainContentRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly trainContentService: TrainContentService,
|
||||
) { }
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
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)
|
||||
}),
|
||||
findFirst:this.trpc.procedure.input(TrainContentFindFirstArgsSchema)
|
||||
.query(async ({input})=>{
|
||||
return this.trainContentService.findFirst(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);
|
||||
}),
|
||||
findFirst: this.trpc.procedure
|
||||
.input(TrainContentFindFirstArgsSchema)
|
||||
.query(async ({ input }) => {
|
||||
return this.trainContentService.findFirst(input);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,43 +1,41 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseService } from "../base/base.service";
|
||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
|
||||
@Injectable()
|
||||
export class TrainContentService extends BaseService<Prisma.TrainContentDelegate> {
|
||||
constructor() {
|
||||
super(db,ObjectType.TRAIN_CONTENT,true);
|
||||
}
|
||||
async create(args: Prisma.TrainContentCreateArgs) {
|
||||
console.log(args)
|
||||
const result = await super.create(args)
|
||||
this.emitDataChanged(CrudOperation.CREATED,result)
|
||||
return result
|
||||
}
|
||||
constructor() {
|
||||
super(db, ObjectType.TRAIN_CONTENT, true);
|
||||
}
|
||||
async create(args: Prisma.TrainContentCreateArgs) {
|
||||
console.log(args);
|
||||
const result = await super.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(args:Prisma.TrainContentUpdateArgs){
|
||||
const result = await super.update(args)
|
||||
this.emitDataChanged(CrudOperation.UPDATED,result)
|
||||
return result
|
||||
}
|
||||
async update(args: Prisma.TrainContentUpdateArgs) {
|
||||
const result = await super.update(args);
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.TrainContentFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
async findFirst(args: Prisma.TrainContentFindFirstArgs) {
|
||||
const result = await super.findFirst(args);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(args: Prisma.TrainContentFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
async findFirst(args: Prisma.TrainContentFindFirstArgs) {
|
||||
const result = await super.findFirst(args)
|
||||
return result
|
||||
}
|
||||
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type:ObjectType.TRAIN_SITUATION,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.TRAIN_SITUATION,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Controller, UseGuards } from "@nestjs/common";
|
||||
import { TrainSituationService } from "./trainSituation.service";
|
||||
import { Controller, UseGuards } from '@nestjs/common';
|
||||
import { TrainSituationService } from './trainSituation.service';
|
||||
import { AuthGuard } from '@server/auth/auth.guard';
|
||||
|
||||
@Controller('train-situation')
|
||||
export class TrainSituationController {
|
||||
constructor(private readonly trainContentService: TrainSituationService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
constructor(private readonly trainContentService: TrainSituationService) {}
|
||||
//@UseGuards(AuthGuard)
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import { TrainSituationRouter } from './trainSituation.router';
|
|||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { SportStandardModule } from '../sport-standard/sportStandard.module';
|
||||
@Module({
|
||||
imports: [StaffModule,SportStandardModule],
|
||||
imports: [StaffModule, SportStandardModule],
|
||||
controllers: [TrainSituationController],
|
||||
providers: [TrainSituationService,TrainSituationRouter,TrpcService],
|
||||
exports: [TrainSituationService,TrainSituationRouter],
|
||||
providers: [TrainSituationService, TrainSituationRouter, TrpcService],
|
||||
exports: [TrainSituationService, TrainSituationRouter],
|
||||
})
|
||||
export class TrainSituationModule {}
|
||||
export class TrainSituationModule {}
|
||||
|
|
|
@ -1,71 +1,80 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { TrpcService } from "@server/trpc/trpc.service";
|
||||
import { TrainSituationService } from "./trainSituation.service";
|
||||
import { z, ZodType } from "zod";
|
||||
import { Prisma } from "@nice/common";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { TrainSituationService } from './trainSituation.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
import { Prisma } from '@nice/common';
|
||||
|
||||
const TrainSituationArgsSchema: ZodType<Prisma.TrainSituationCreateArgs> = z.any()
|
||||
const TrainSituationUpdateArgsSchema: ZodType<Prisma.TrainSituationUpdateArgs> = z.any()
|
||||
const TrainSituationFindManyArgsSchema: ZodType<Prisma.TrainSituationFindManyArgs> = z.any()
|
||||
const TrainSituationArgsSchema: ZodType<Prisma.TrainSituationCreateArgs> =
|
||||
z.any();
|
||||
const TrainSituationUpdateArgsSchema: ZodType<Prisma.TrainSituationUpdateArgs> =
|
||||
z.any();
|
||||
const TrainSituationFindManyArgsSchema: ZodType<Prisma.TrainSituationFindManyArgs> =
|
||||
z.any();
|
||||
|
||||
@Injectable()
|
||||
export class TrainSituationRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
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)
|
||||
}),
|
||||
createManyTrainSituation: this.trpc.protectProcedure
|
||||
.input(z.array(
|
||||
z.object({
|
||||
staffId: z.string(),
|
||||
trainContentId: z.string(),
|
||||
mustTrainTime: z.number(),
|
||||
alreadyTrainTime: z.number(),
|
||||
value: z.string(),
|
||||
projectUnit: z.string(),
|
||||
personType: z.string(),
|
||||
projectId: z.string(),
|
||||
gender: z.boolean(),
|
||||
age: z.number(),
|
||||
performance: z.number().or(z.string())
|
||||
})
|
||||
))
|
||||
.mutation(async ({ input }) => {
|
||||
console.log(input)
|
||||
return this.trainSituationService.createManyTrainSituation(input)
|
||||
}),
|
||||
deleteSameGroupTrainSituation:this.trpc.protectProcedure
|
||||
.input(z.object({
|
||||
groupId:z.string()
|
||||
}))
|
||||
.mutation(async ({input})=>{
|
||||
return this.trainSituationService.deleteSameGroupTrainSituation(input)
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
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(),
|
||||
date: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
return this.trainSituationService.findManyByDeptId(input);
|
||||
}),
|
||||
createManyTrainSituation: this.trpc.protectProcedure
|
||||
.input(
|
||||
z.array(
|
||||
z.object({
|
||||
staffId: z.string(),
|
||||
trainContentId: z.string(),
|
||||
mustTrainTime: z.number(),
|
||||
alreadyTrainTime: z.number(),
|
||||
value: z.string(),
|
||||
projectUnit: z.string(),
|
||||
personType: z.string(),
|
||||
projectId: z.string(),
|
||||
gender: z.boolean(),
|
||||
age: z.number(),
|
||||
performance: z.number().or(z.string()),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
console.log(input);
|
||||
return this.trainSituationService.createManyTrainSituation(input);
|
||||
}),
|
||||
deleteSameGroupTrainSituation: this.trpc.protectProcedure
|
||||
.input(
|
||||
z.object({
|
||||
groupId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
return this.trainSituationService.deleteSameGroupTrainSituation(input);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,145 +1,192 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { BaseService } from "../base/base.service";
|
||||
import { db, ObjectType, Prisma, UserProfile } from "@nice/common";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
import { DefaultArgs } from "@prisma/client/runtime/library";
|
||||
import { StaffService } from "../staff/staff.service";
|
||||
import { SportStandardService } from "../sport-standard/sportStandard.service";
|
||||
import { uuidv4 } from "lib0/random";
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import { db, ObjectType, Prisma, UserProfile } from '@nice/common';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
import { DefaultArgs } from '@prisma/client/runtime/library';
|
||||
import { StaffService } from '../staff/staff.service';
|
||||
import { SportStandardService } from '../sport-standard/sportStandard.service';
|
||||
import { uuidv4 } from 'lib0/random';
|
||||
|
||||
@Injectable()
|
||||
export class TrainSituationService extends BaseService<Prisma.TrainSituationDelegate> {
|
||||
constructor(private readonly staffService:StaffService,private readonly sportStandardService:SportStandardService) {
|
||||
super(db,ObjectType.TRAIN_SITUATION,false);
|
||||
}
|
||||
// 创建培训情况
|
||||
async create(
|
||||
args: Prisma.TrainSituationCreateArgs,
|
||||
){
|
||||
console.log(args)
|
||||
const result = await super.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
// 更新培训情况
|
||||
async update(
|
||||
args: Prisma.TrainSituationUpdateArgs,
|
||||
){
|
||||
const result = await super.update(args);
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 查找培训情况
|
||||
async findMany(args: Prisma.TrainSituationFindManyArgs)
|
||||
constructor(
|
||||
private readonly staffService: StaffService,
|
||||
private readonly sportStandardService: SportStandardService,
|
||||
) {
|
||||
super(db, ObjectType.TRAIN_SITUATION, false);
|
||||
}
|
||||
// 创建培训情况
|
||||
async create(args: Prisma.TrainSituationCreateArgs) {
|
||||
console.log(args);
|
||||
const result = await db.trainSituation.create(args);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
// 更新培训情况
|
||||
async update(args: Prisma.TrainSituationUpdateArgs) {
|
||||
const result = await db.trainSituation.update(args);
|
||||
this.emitDataChanged(CrudOperation.UPDATED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 查找培训情况
|
||||
async findMany(args: Prisma.TrainSituationFindManyArgs) {
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
// 查找某一单位所有人员的培训情况
|
||||
async findManyByDeptId(params: {
|
||||
deptId?: string;
|
||||
domainId?: string;
|
||||
trainContentId?: string;
|
||||
date?: string;
|
||||
}): Promise<
|
||||
{
|
||||
const result = await super.findMany(args);
|
||||
return result;
|
||||
}
|
||||
// 查找某一单位所有人员的培训情况
|
||||
async findManyByDeptId(args:{
|
||||
deptId?:string,
|
||||
domainId?:string,
|
||||
trainContentId?:string
|
||||
}):Promise<{
|
||||
id: string;
|
||||
staffId: string;
|
||||
trainContentId: string;
|
||||
mustTrainTime: number;
|
||||
alreadyTrainTime: number;
|
||||
score: number;
|
||||
}[]>
|
||||
{
|
||||
const staffs = await this.staffService.findByDept({deptId:args.deptId,domainId:args.domainId})
|
||||
const result = await super.findMany({
|
||||
where:{
|
||||
staffId:{
|
||||
in:staffs.map(staff=>staff.id)
|
||||
},
|
||||
...(args.trainContentId ? {trainContentId:args.trainContentId} : {})
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
//async createDailyTrainTime()
|
||||
async createTrainSituation(args:{
|
||||
staffId?:string,
|
||||
trainContentId?:string,
|
||||
mustTrainTime?:number,
|
||||
alreadyTrainTime?:number,
|
||||
value?:string,
|
||||
id: string;
|
||||
staffId: string;
|
||||
trainContentId: string;
|
||||
mustTrainTime: number;
|
||||
alreadyTrainTime: number;
|
||||
score: number;
|
||||
}[]
|
||||
> {
|
||||
const { deptId, domainId, trainContentId, date } = params;
|
||||
|
||||
projectUnit?:string,
|
||||
personType?:string,
|
||||
projectId?:string,
|
||||
gender?:boolean,
|
||||
age?:number,
|
||||
performance?:number|string,
|
||||
},groupId?:string){
|
||||
console.log("传入的参数",args)
|
||||
const score = await this.sportStandardService.getScore({
|
||||
projectId:args.projectId,
|
||||
gender:args.gender,
|
||||
age:args.age,
|
||||
performance:args.performance,
|
||||
personType:args.personType,
|
||||
projectUnit:args.projectUnit
|
||||
})
|
||||
console.log("计算出的分数",score)
|
||||
const data : Prisma.TrainSituationCreateArgs = {
|
||||
data:{
|
||||
staffId:args.staffId,
|
||||
trainContentId:args.trainContentId,
|
||||
mustTrainTime:args.mustTrainTime,
|
||||
alreadyTrainTime:args.alreadyTrainTime,
|
||||
value:args.value,
|
||||
score:score,
|
||||
groupId:groupId
|
||||
}
|
||||
}
|
||||
console.log("创建的数据",data)
|
||||
const result = await super.create(data);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
async createManyTrainSituation(args:{
|
||||
staffId?:string,
|
||||
trainContentId?:string,
|
||||
mustTrainTime?:number,
|
||||
alreadyTrainTime?:number,
|
||||
value?:string,
|
||||
// 构建查询条件
|
||||
const where: Prisma.TrainSituationWhereInput = {};
|
||||
|
||||
projectUnit?:string,
|
||||
personType?:string,
|
||||
projectId?:string,
|
||||
gender?:boolean,
|
||||
age?:number,
|
||||
performance?:number|string,
|
||||
}[]){
|
||||
console.log("传入的参数",args)
|
||||
const groupId = uuidv4();
|
||||
args.forEach(async (item)=>{
|
||||
await this.createTrainSituation(item,groupId)
|
||||
})
|
||||
}
|
||||
async deleteSameGroupTrainSituation(args:{groupId?:string}){
|
||||
const {groupId} = args
|
||||
const result = await super.deleteMany({
|
||||
where:{
|
||||
groupId
|
||||
}
|
||||
})
|
||||
this.emitDataChanged(CrudOperation.DELETED,result)
|
||||
return result
|
||||
if (deptId) {
|
||||
const staffs = await this.staffService.findManyByDeptId(deptId);
|
||||
where.staffId = { in: staffs.map((s) => s.id) };
|
||||
}
|
||||
|
||||
// 发送数据变化事件
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type:ObjectType.TRAIN_SITUATION,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
if (trainContentId) {
|
||||
where.trainContentId = trainContentId;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加日期过滤条件
|
||||
if (date) {
|
||||
// 创建日期的开始时间(当天的00:00:00)
|
||||
const startDate = new Date(date);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// 创建日期的结束时间(当天的23:59:59.999)
|
||||
const endDate = new Date(date);
|
||||
endDate.setHours(23, 59, 59, 999);
|
||||
|
||||
// 设置createdAt字段在指定的日期范围内
|
||||
where.createdAt = {
|
||||
gte: startDate,
|
||||
lte: endDate,
|
||||
};
|
||||
|
||||
// 日志输出,方便调试
|
||||
console.log(
|
||||
`Filtering train situations between ${startDate.toISOString()} and ${endDate.toISOString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 查询结果并包含关联数据
|
||||
const result = await super.findMany({
|
||||
where,
|
||||
include: {
|
||||
staff: {
|
||||
include: {
|
||||
department: true,
|
||||
position: true,
|
||||
},
|
||||
},
|
||||
trainContent: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc', // 按创建时间倒序排列,最新的记录在前
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
//async createDailyTrainTime()
|
||||
async createTrainSituation(
|
||||
args: {
|
||||
staffId?: string;
|
||||
trainContentId?: string;
|
||||
mustTrainTime?: number;
|
||||
alreadyTrainTime?: number;
|
||||
value?: string;
|
||||
|
||||
projectUnit?: string;
|
||||
personType?: string;
|
||||
projectId?: string;
|
||||
gender?: boolean;
|
||||
age?: number;
|
||||
performance?: number | string;
|
||||
},
|
||||
groupId?: string,
|
||||
) {
|
||||
console.log('传入的参数', args);
|
||||
const score = await this.sportStandardService.getScore({
|
||||
projectId: args.projectId,
|
||||
gender: args.gender,
|
||||
age: args.age,
|
||||
performance: args.performance,
|
||||
personType: args.personType,
|
||||
projectUnit: args.projectUnit,
|
||||
});
|
||||
console.log('计算出的分数', score);
|
||||
const data: Prisma.TrainSituationCreateArgs = {
|
||||
data: {
|
||||
staffId: args.staffId,
|
||||
trainContentId: args.trainContentId,
|
||||
mustTrainTime: args.mustTrainTime,
|
||||
alreadyTrainTime: args.alreadyTrainTime,
|
||||
value: args.value,
|
||||
score: score,
|
||||
groupId: groupId,
|
||||
},
|
||||
};
|
||||
console.log('创建的数据', data);
|
||||
const result = await super.create(data);
|
||||
this.emitDataChanged(CrudOperation.CREATED, result);
|
||||
return result;
|
||||
}
|
||||
async createManyTrainSituation(
|
||||
args: {
|
||||
staffId?: string;
|
||||
trainContentId?: string;
|
||||
mustTrainTime?: number;
|
||||
alreadyTrainTime?: number;
|
||||
value?: string;
|
||||
|
||||
projectUnit?: string;
|
||||
personType?: string;
|
||||
projectId?: string;
|
||||
gender?: boolean;
|
||||
age?: number;
|
||||
performance?: number | string;
|
||||
}[],
|
||||
) {
|
||||
console.log('传入的参数', args);
|
||||
const groupId = uuidv4();
|
||||
args.forEach(async (item) => {
|
||||
await this.createTrainSituation(item, groupId);
|
||||
});
|
||||
}
|
||||
async deleteSameGroupTrainSituation(args: { groupId?: string }) {
|
||||
const { groupId } = args;
|
||||
const result = await super.deleteMany({
|
||||
where: {
|
||||
groupId,
|
||||
},
|
||||
});
|
||||
this.emitDataChanged(CrudOperation.DELETED, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 发送数据变化事件
|
||||
private emitDataChanged(operation: CrudOperation, data: any) {
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.TRAIN_SITUATION,
|
||||
operation,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,7 @@ import { DepartmentModule } from '../department/department.module';
|
|||
import { StaffModule } from '../staff/staff.module';
|
||||
// import { TransformController } from './transform.controller';
|
||||
@Module({
|
||||
imports: [
|
||||
DepartmentModule,
|
||||
StaffModule,
|
||||
TermModule,
|
||||
TaxonomyModule,
|
||||
],
|
||||
imports: [DepartmentModule, StaffModule, TermModule, TaxonomyModule],
|
||||
providers: [TransformService, TransformRouter, TrpcService],
|
||||
exports: [TransformRouter, TransformService],
|
||||
// controllers:[TransformController]
|
||||
|
|
|
@ -5,6 +5,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
|||
|
||||
@Module({
|
||||
providers: [VisitService, VisitRouter, TrpcService],
|
||||
exports: [VisitRouter]
|
||||
exports: [VisitRouter],
|
||||
})
|
||||
export class VisitModule { }
|
||||
export class VisitModule {}
|
||||
|
|
|
@ -1,205 +1,210 @@
|
|||
|
||||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { WebSocketServerConfig, WSClient, WebSocketType } from "../types";
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { WebSocketServerConfig, WSClient, WebSocketType } from '../types';
|
||||
import { SocketMessage } from '@nice/common';
|
||||
|
||||
const DEFAULT_CONFIG: WebSocketServerConfig = {
|
||||
pingInterval: 30000,
|
||||
pingTimeout: 5000,
|
||||
debug: false, // 新增默认调试配置
|
||||
pingInterval: 30000,
|
||||
pingTimeout: 5000,
|
||||
debug: false, // 新增默认调试配置
|
||||
};
|
||||
interface IWebSocketServer {
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
broadcast(data: any): void;
|
||||
handleConnection(ws: WSClient): void;
|
||||
handleDisconnection(ws: WSClient): void;
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
broadcast(data: any): void;
|
||||
handleConnection(ws: WSClient): void;
|
||||
handleDisconnection(ws: WSClient): void;
|
||||
}
|
||||
|
||||
export abstract class BaseWebSocketServer implements IWebSocketServer {
|
||||
private _wss: WebSocketServer | null = null;
|
||||
protected clients: Set<WSClient> = new Set();
|
||||
protected timeouts: Map<WSClient, NodeJS.Timeout> = new Map();
|
||||
protected pingIntervalId?: NodeJS.Timeout;
|
||||
protected readonly logger = new Logger(this.constructor.name);
|
||||
protected readonly finalConfig: WebSocketServerConfig;
|
||||
private userClientMap: Map<string, WSClient> = new Map();
|
||||
constructor(
|
||||
protected readonly config: Partial<WebSocketServerConfig> = {}
|
||||
) {
|
||||
this.finalConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
...config,
|
||||
};
|
||||
private _wss: WebSocketServer | null = null;
|
||||
protected clients: Set<WSClient> = new Set();
|
||||
protected timeouts: Map<WSClient, NodeJS.Timeout> = new Map();
|
||||
protected pingIntervalId?: NodeJS.Timeout;
|
||||
protected readonly logger = new Logger(this.constructor.name);
|
||||
protected readonly finalConfig: WebSocketServerConfig;
|
||||
private userClientMap: Map<string, WSClient> = new Map();
|
||||
constructor(protected readonly config: Partial<WebSocketServerConfig> = {}) {
|
||||
this.finalConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
...config,
|
||||
};
|
||||
}
|
||||
protected debugLog(message: string, ...optionalParams: any[]): void {
|
||||
if (this.finalConfig.debug) {
|
||||
this.logger.debug(message, ...optionalParams);
|
||||
}
|
||||
protected debugLog(message: string, ...optionalParams: any[]): void {
|
||||
if (this.finalConfig.debug) {
|
||||
this.logger.debug(message, ...optionalParams);
|
||||
}
|
||||
}
|
||||
public getClientCount() {
|
||||
return this.clients.size
|
||||
}
|
||||
// 暴露 WebSocketServer 实例的只读访问
|
||||
public get wss(): WebSocketServer | null {
|
||||
return this._wss;
|
||||
}
|
||||
public getClientCount() {
|
||||
return this.clients.size;
|
||||
}
|
||||
// 暴露 WebSocketServer 实例的只读访问
|
||||
public get wss(): WebSocketServer | null {
|
||||
return this._wss;
|
||||
}
|
||||
|
||||
// 内部使用的 setter
|
||||
protected set wss(value: WebSocketServer | null) {
|
||||
this._wss = value;
|
||||
}
|
||||
|
||||
public abstract get serverType(): WebSocketType;
|
||||
|
||||
public get serverPath(): string {
|
||||
return this.finalConfig.path || `/${this.serverType}`;
|
||||
}
|
||||
|
||||
public async start(): Promise<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
|
||||
protected set wss(value: WebSocketServer | null) {
|
||||
this._wss = value;
|
||||
this.clients.forEach((client) => client.close());
|
||||
this.clients.clear();
|
||||
this.timeouts.clear();
|
||||
|
||||
if (this._wss) {
|
||||
await new Promise((resolve) => this._wss!.close(resolve));
|
||||
this._wss = null;
|
||||
}
|
||||
|
||||
public abstract get serverType(): WebSocketType;
|
||||
this.debugLog(`WebSocket server stopped on path: ${this.serverPath}`);
|
||||
}
|
||||
|
||||
public get serverPath(): string {
|
||||
return this.finalConfig.path || `/${this.serverType}`;
|
||||
public broadcast(data: SocketMessage): void {
|
||||
this.clients.forEach(
|
||||
(client) =>
|
||||
client.readyState === WebSocket.OPEN &&
|
||||
client.send(JSON.stringify(data)),
|
||||
);
|
||||
}
|
||||
public sendToUser(id: string, data: SocketMessage) {
|
||||
const message = JSON.stringify(data);
|
||||
const client = this.userClientMap.get(id);
|
||||
client?.send(message);
|
||||
}
|
||||
public sendToUsers(ids: string[], data: SocketMessage) {
|
||||
const message = JSON.stringify(data);
|
||||
ids.forEach((id) => {
|
||||
const client = this.userClientMap.get(id);
|
||||
client?.send(message);
|
||||
});
|
||||
}
|
||||
public sendToRoom(roomId: string, data: SocketMessage) {
|
||||
const message = JSON.stringify(data);
|
||||
this.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN && client.roomId === roomId) {
|
||||
client.send(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
protected getRoomClientsCount(roomId?: string): number {
|
||||
if (!roomId) return 0;
|
||||
return Array.from(this.clients).filter((client) => client.roomId === roomId)
|
||||
.length;
|
||||
}
|
||||
|
||||
public handleConnection(ws: WSClient): void {
|
||||
if (ws.userId) {
|
||||
this.userClientMap.set(ws.userId, ws);
|
||||
}
|
||||
ws.isAlive = true;
|
||||
ws.type = this.serverType;
|
||||
this.clients.add(ws);
|
||||
this.setupClientEvents(ws);
|
||||
|
||||
public async start(): Promise<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;
|
||||
}
|
||||
|
||||
this.clients.forEach(client => client.close());
|
||||
this.clients.clear();
|
||||
this.timeouts.clear();
|
||||
|
||||
if (this._wss) {
|
||||
await new Promise(resolve => this._wss!.close(resolve));
|
||||
this._wss = null;
|
||||
}
|
||||
|
||||
this.debugLog(`WebSocket server stopped on path: ${this.serverPath}`);
|
||||
}
|
||||
|
||||
public broadcast(data: SocketMessage): void {
|
||||
this.clients.forEach(client =>
|
||||
client.readyState === WebSocket.OPEN && client.send(JSON.stringify(data))
|
||||
);
|
||||
}
|
||||
public sendToUser(id: string, data: SocketMessage) {
|
||||
const message = JSON.stringify(data);
|
||||
const client = this.userClientMap.get(id);
|
||||
client?.send(message)
|
||||
}
|
||||
public sendToUsers(ids: string[], data: SocketMessage) {
|
||||
const message = JSON.stringify(data);
|
||||
ids.forEach(id => {
|
||||
const client = this.userClientMap.get(id);
|
||||
client?.send(message);
|
||||
});
|
||||
}
|
||||
public sendToRoom(roomId: string, data: SocketMessage) {
|
||||
const message = JSON.stringify(data);
|
||||
this.clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN && client.roomId === roomId) {
|
||||
client.send(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
protected getRoomClientsCount(roomId?: string): number {
|
||||
if (!roomId) return 0;
|
||||
return Array.from(this.clients).filter(client => client.roomId === roomId).length;
|
||||
}
|
||||
|
||||
public handleConnection(ws: WSClient): void {
|
||||
if (ws.userId) {
|
||||
this.userClientMap.set(ws.userId, ws);
|
||||
}
|
||||
ws.isAlive = true;
|
||||
ws.type = this.serverType;
|
||||
this.clients.add(ws);
|
||||
this.setupClientEvents(ws);
|
||||
|
||||
const roomClientsCount = this.getRoomClientsCount(ws.roomId);
|
||||
this.debugLog(`
|
||||
const roomClientsCount = this.getRoomClientsCount(ws.roomId);
|
||||
this.debugLog(`
|
||||
[${this.serverType}] connected
|
||||
userId ${ws.userId}
|
||||
roomId ${ws.roomId}
|
||||
room clients ${roomClientsCount}
|
||||
total clients ${this.clients.size}`);
|
||||
}
|
||||
|
||||
public handleDisconnection(ws: WSClient): void {
|
||||
if (ws.userId) {
|
||||
this.userClientMap.delete(ws.userId);
|
||||
}
|
||||
this.clients.delete(ws);
|
||||
const timeout = this.timeouts.get(ws);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this.timeouts.delete(ws);
|
||||
}
|
||||
ws.terminate();
|
||||
|
||||
public handleDisconnection(ws: WSClient): void {
|
||||
if (ws.userId) {
|
||||
this.userClientMap.delete(ws.userId);
|
||||
}
|
||||
this.clients.delete(ws);
|
||||
const timeout = this.timeouts.get(ws);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this.timeouts.delete(ws);
|
||||
}
|
||||
ws.terminate();
|
||||
const roomClientsCount = this.getRoomClientsCount(ws.roomId);
|
||||
|
||||
const roomClientsCount = this.getRoomClientsCount(ws.roomId);
|
||||
|
||||
this.debugLog(`
|
||||
this.debugLog(`
|
||||
[${this.serverType}] disconnected
|
||||
userId ${ws.userId}
|
||||
roomId ${ws.roomId}
|
||||
room clients ${roomClientsCount}
|
||||
total clients ${this.clients.size}`);
|
||||
}
|
||||
protected setupClientEvents(ws: WSClient): void {
|
||||
ws.on('pong', () => this.handlePong(ws))
|
||||
.on('close', () => this.handleDisconnection(ws))
|
||||
.on('error', (error) => {
|
||||
this.logger.error(`[${this.serverType}] client error on path ${this.serverPath}:`, error);
|
||||
this.handleDisconnection(ws);
|
||||
});
|
||||
}
|
||||
|
||||
private handlePong(ws: WSClient): void {
|
||||
ws.isAlive = true;
|
||||
const timeout = this.timeouts.get(ws);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this.timeouts.delete(ws);
|
||||
}
|
||||
}
|
||||
|
||||
private startPingInterval(): void {
|
||||
this.pingIntervalId = setInterval(
|
||||
() => this.pingClients(),
|
||||
this.finalConfig.pingInterval
|
||||
}
|
||||
protected setupClientEvents(ws: WSClient): void {
|
||||
ws.on('pong', () => this.handlePong(ws))
|
||||
.on('close', () => this.handleDisconnection(ws))
|
||||
.on('error', (error) => {
|
||||
this.logger.error(
|
||||
`[${this.serverType}] client error on path ${this.serverPath}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
this.handleDisconnection(ws);
|
||||
});
|
||||
}
|
||||
|
||||
private pingClients(): void {
|
||||
this.clients.forEach(ws => {
|
||||
if (!ws.isAlive) return this.handleDisconnection(ws);
|
||||
|
||||
ws.isAlive = false;
|
||||
ws.ping();
|
||||
const timeout = setTimeout(
|
||||
() => !ws.isAlive && this.handleDisconnection(ws),
|
||||
this.finalConfig.pingTimeout
|
||||
);
|
||||
this.timeouts.set(ws, timeout);
|
||||
});
|
||||
private handlePong(ws: WSClient): void {
|
||||
ws.isAlive = true;
|
||||
const timeout = this.timeouts.get(ws);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this.timeouts.delete(ws);
|
||||
}
|
||||
}
|
||||
|
||||
protected setupServerEvents(): void {
|
||||
if (!this._wss) return;
|
||||
this._wss
|
||||
.on('connection', (ws: WSClient) => this.handleConnection(ws))
|
||||
.on('error', (error) => this.logger.error(`Server error on path ${this.serverPath}:`, error));
|
||||
}
|
||||
private startPingInterval(): void {
|
||||
this.pingIntervalId = setInterval(
|
||||
() => this.pingClients(),
|
||||
this.finalConfig.pingInterval,
|
||||
);
|
||||
}
|
||||
|
||||
private pingClients(): void {
|
||||
this.clients.forEach((ws) => {
|
||||
if (!ws.isAlive) return this.handleDisconnection(ws);
|
||||
|
||||
ws.isAlive = false;
|
||||
ws.ping();
|
||||
const timeout = setTimeout(
|
||||
() => !ws.isAlive && this.handleDisconnection(ws),
|
||||
this.finalConfig.pingTimeout,
|
||||
);
|
||||
this.timeouts.set(ws, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
protected setupServerEvents(): void {
|
||||
if (!this._wss) return;
|
||||
this._wss
|
||||
.on('connection', (ws: WSClient) => this.handleConnection(ws))
|
||||
.on('error', (error) =>
|
||||
this.logger.error(`Server error on path ${this.serverPath}:`, error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,13 @@ import http from 'http';
|
|||
import { parseInt as libParseInt } from 'lib0/number';
|
||||
import { WSSharedDoc } from './ws-shared-doc';
|
||||
|
||||
|
||||
/**
|
||||
* 回调URL配置,从环境变量中获取
|
||||
* 如果环境变量未设置则为null
|
||||
*/
|
||||
const CALLBACK_URL = process.env.CALLBACK_URL ? new URL(process.env.CALLBACK_URL) : null;
|
||||
const CALLBACK_URL = process.env.CALLBACK_URL
|
||||
? new URL(process.env.CALLBACK_URL)
|
||||
: null;
|
||||
|
||||
/**
|
||||
* 回调超时时间配置,从环境变量中获取
|
||||
|
@ -25,7 +26,9 @@ const CALLBACK_TIMEOUT = libParseInt(process.env.CALLBACK_TIMEOUT || '5000');
|
|||
* 需要监听变更的共享对象配置
|
||||
* 从环境变量CALLBACK_OBJECTS中解析JSON格式的配置
|
||||
*/
|
||||
const CALLBACK_OBJECTS: Record<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是否已配置的标志
|
||||
|
@ -37,10 +40,13 @@ export const isCallbackSet = !!CALLBACK_URL;
|
|||
*/
|
||||
interface DataToSend {
|
||||
room: string; // 房间/文档标识
|
||||
data: Record<string, {
|
||||
type: string; // 数据类型
|
||||
content: any; // 数据内容
|
||||
}>;
|
||||
data: Record<
|
||||
string,
|
||||
{
|
||||
type: string; // 数据类型
|
||||
content: any; // 数据内容
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -59,25 +65,29 @@ type OriginType = any;
|
|||
* @param origin - 更新的来源
|
||||
* @param doc - 共享文档实例
|
||||
*/
|
||||
export const callbackHandler = (update: UpdateType, origin: OriginType, doc: WSSharedDoc): void => {
|
||||
export const callbackHandler = (
|
||||
update: UpdateType,
|
||||
origin: OriginType,
|
||||
doc: WSSharedDoc,
|
||||
): void => {
|
||||
// 获取文档名称作为房间标识
|
||||
const room = doc.name;
|
||||
|
||||
|
||||
// 初始化要发送的数据对象
|
||||
const dataToSend: DataToSend = {
|
||||
room,
|
||||
data: {}
|
||||
data: {},
|
||||
};
|
||||
|
||||
// 获取所有需要监听的共享对象名称
|
||||
const sharedObjectList = Object.keys(CALLBACK_OBJECTS);
|
||||
|
||||
|
||||
// 遍历所有共享对象,获取它们的最新内容
|
||||
sharedObjectList.forEach(sharedObjectName => {
|
||||
sharedObjectList.forEach((sharedObjectName) => {
|
||||
const sharedObjectType = CALLBACK_OBJECTS[sharedObjectName];
|
||||
dataToSend.data[sharedObjectName] = {
|
||||
type: sharedObjectType,
|
||||
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON()
|
||||
content: getContent(sharedObjectName, sharedObjectType, doc).toJSON(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -106,8 +116,8 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
|
|||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(dataString)
|
||||
}
|
||||
'Content-Length': Buffer.byteLength(dataString),
|
||||
},
|
||||
};
|
||||
|
||||
// 创建HTTP请求
|
||||
|
@ -137,14 +147,24 @@ const callbackRequest = (url: URL, timeout: number, data: DataToSend): void => {
|
|||
* @param doc - 共享文档实例
|
||||
* @returns 共享对象的内容
|
||||
*/
|
||||
const getContent = (objName: string, objType: string, doc: WSSharedDoc): any => {
|
||||
const getContent = (
|
||||
objName: string,
|
||||
objType: string,
|
||||
doc: WSSharedDoc,
|
||||
): any => {
|
||||
// 根据对象类型返回相应的共享对象
|
||||
switch (objType) {
|
||||
case 'Array': return doc.getArray(objName);
|
||||
case 'Map': return doc.getMap(objName);
|
||||
case 'Text': return doc.getText(objName);
|
||||
case 'XmlFragment': return doc.getXmlFragment(objName);
|
||||
case 'XmlElement': return doc.getXmlElement(objName);
|
||||
default: return {};
|
||||
case 'Array':
|
||||
return doc.getArray(objName);
|
||||
case 'Map':
|
||||
return doc.getMap(objName);
|
||||
case 'Text':
|
||||
return doc.getText(objName);
|
||||
case 'XmlFragment':
|
||||
return doc.getXmlFragment(objName);
|
||||
case 'XmlElement':
|
||||
return doc.getXmlElement(objName);
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,6 +3,6 @@ import { YjsServer } from './yjs.server';
|
|||
|
||||
@Module({
|
||||
providers: [YjsServer],
|
||||
exports: [YjsServer]
|
||||
exports: [YjsServer],
|
||||
})
|
||||
export class CollaborationModule { }
|
||||
export class CollaborationModule {}
|
||||
|
|
|
@ -23,7 +23,7 @@ if (typeof persistenceDir === 'string') {
|
|||
ldb.storeUpdate(docName, update);
|
||||
});
|
||||
},
|
||||
writeState: async (_docName, _ydoc) => { },
|
||||
writeState: async (_docName, _ydoc) => {},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export interface ConnectionOptions {
|
||||
docName: string;
|
||||
gc: boolean;
|
||||
}
|
||||
|
||||
docName: string;
|
||||
gc: boolean;
|
||||
}
|
||||
|
|
|
@ -1,158 +1,187 @@
|
|||
import { readSyncMessage } from '@nice/common';
|
||||
import { applyAwarenessUpdate, Awareness, encodeAwarenessUpdate, removeAwarenessStates, writeSyncStep1, writeUpdate } from '@nice/common';
|
||||
import {
|
||||
applyAwarenessUpdate,
|
||||
Awareness,
|
||||
encodeAwarenessUpdate,
|
||||
removeAwarenessStates,
|
||||
writeSyncStep1,
|
||||
writeUpdate,
|
||||
} from '@nice/common';
|
||||
import * as encoding from 'lib0/encoding';
|
||||
import * as decoding from 'lib0/decoding';
|
||||
import * as Y from "yjs"
|
||||
import * as Y from 'yjs';
|
||||
import { debounce } from 'lodash';
|
||||
import { getPersistence, setPersistence } from './persistence';
|
||||
import { callbackHandler, isCallbackSet } from './callback';
|
||||
import { WebSocket } from "ws";
|
||||
import { WebSocket } from 'ws';
|
||||
import { YMessageType } from '@nice/common';
|
||||
import { WSClient } from '../types';
|
||||
export const docs = new Map<string, WSSharedDoc>();
|
||||
export const CALLBACK_DEBOUNCE_WAIT = parseInt(process.env.CALLBACK_DEBOUNCE_WAIT || '2000');
|
||||
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000');
|
||||
export const CALLBACK_DEBOUNCE_WAIT = parseInt(
|
||||
process.env.CALLBACK_DEBOUNCE_WAIT || '2000',
|
||||
);
|
||||
export const CALLBACK_DEBOUNCE_MAXWAIT = parseInt(
|
||||
process.env.CALLBACK_DEBOUNCE_MAXWAIT || '10000',
|
||||
);
|
||||
export const getYDoc = (docname: string, gc = true): WSSharedDoc => {
|
||||
return docs.get(docname) || createYDoc(docname, gc);
|
||||
return docs.get(docname) || createYDoc(docname, gc);
|
||||
};
|
||||
const createYDoc = (docname: string, gc: boolean): WSSharedDoc => {
|
||||
const doc = new WSSharedDoc(docname, gc);
|
||||
docs.set(docname, doc);
|
||||
return doc;
|
||||
const doc = new WSSharedDoc(docname, gc);
|
||||
docs.set(docname, doc);
|
||||
return doc;
|
||||
};
|
||||
|
||||
export const send = (doc: WSSharedDoc, conn: WebSocket, m: Uint8Array) => {
|
||||
if (conn.readyState !== WebSocket.OPEN) {
|
||||
closeConn(doc, conn);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
conn.send(m, {}, err => { err != null && closeConn(doc, conn) });
|
||||
} catch (e) {
|
||||
closeConn(doc, conn);
|
||||
}
|
||||
if (conn.readyState !== WebSocket.OPEN) {
|
||||
closeConn(doc, conn);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
conn.send(m, {}, (err) => {
|
||||
err != null && closeConn(doc, conn);
|
||||
});
|
||||
} catch (e) {
|
||||
closeConn(doc, conn);
|
||||
}
|
||||
};
|
||||
export const closeConn = (doc: WSSharedDoc, conn: WebSocket) => {
|
||||
if (doc.conns.has(conn)) {
|
||||
const controlledIds = doc.conns.get(conn) as Set<number>;
|
||||
doc.conns.delete(conn);
|
||||
removeAwarenessStates(
|
||||
doc.awareness,
|
||||
Array.from(controlledIds),
|
||||
null
|
||||
);
|
||||
if (doc.conns.has(conn)) {
|
||||
const controlledIds = doc.conns.get(conn) as Set<number>;
|
||||
doc.conns.delete(conn);
|
||||
removeAwarenessStates(doc.awareness, Array.from(controlledIds), null);
|
||||
|
||||
if (doc.conns.size === 0 && getPersistence() !== null) {
|
||||
getPersistence()?.writeState(doc.name, doc).then(() => {
|
||||
doc.destroy();
|
||||
});
|
||||
docs.delete(doc.name);
|
||||
}
|
||||
if (doc.conns.size === 0 && getPersistence() !== null) {
|
||||
getPersistence()
|
||||
?.writeState(doc.name, doc)
|
||||
.then(() => {
|
||||
doc.destroy();
|
||||
});
|
||||
docs.delete(doc.name);
|
||||
}
|
||||
conn.close();
|
||||
}
|
||||
conn.close();
|
||||
};
|
||||
|
||||
export const messageListener = (conn: WSClient, doc: WSSharedDoc, message: Uint8Array) => {
|
||||
try {
|
||||
const encoder = encoding.createEncoder();
|
||||
const decoder = decoding.createDecoder(message);
|
||||
const messageType = decoding.readVarUint(decoder);
|
||||
switch (messageType) {
|
||||
case YMessageType.Sync:
|
||||
// console.log(`received sync message ${message.length}`)
|
||||
encoding.writeVarUint(encoder, YMessageType.Sync);
|
||||
readSyncMessage(decoder, encoder, doc, conn);
|
||||
if (encoding.length(encoder) > 1) {
|
||||
send(doc, conn, encoding.toUint8Array(encoder));
|
||||
}
|
||||
break;
|
||||
|
||||
case YMessageType.Awareness: {
|
||||
applyAwarenessUpdate(
|
||||
doc.awareness,
|
||||
decoding.readVarUint8Array(decoder),
|
||||
conn
|
||||
);
|
||||
// console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`)
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
doc.emit('error' as any, [err]);
|
||||
}
|
||||
};
|
||||
|
||||
const updateHandler = (update: Uint8Array, _origin: any, doc: WSSharedDoc, _tr: any) => {
|
||||
export const messageListener = (
|
||||
conn: WSClient,
|
||||
doc: WSSharedDoc,
|
||||
message: Uint8Array,
|
||||
) => {
|
||||
try {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, YMessageType.Sync);
|
||||
writeUpdate(encoder, update);
|
||||
const message = encoding.toUint8Array(encoder);
|
||||
doc.conns.forEach((_, conn) => send(doc, conn, message));
|
||||
const decoder = decoding.createDecoder(message);
|
||||
const messageType = decoding.readVarUint(decoder);
|
||||
switch (messageType) {
|
||||
case YMessageType.Sync:
|
||||
// console.log(`received sync message ${message.length}`)
|
||||
encoding.writeVarUint(encoder, YMessageType.Sync);
|
||||
readSyncMessage(decoder, encoder, doc, conn);
|
||||
if (encoding.length(encoder) > 1) {
|
||||
send(doc, conn, encoding.toUint8Array(encoder));
|
||||
}
|
||||
break;
|
||||
|
||||
case YMessageType.Awareness: {
|
||||
applyAwarenessUpdate(
|
||||
doc.awareness,
|
||||
decoding.readVarUint8Array(decoder),
|
||||
conn,
|
||||
);
|
||||
// console.log(`received awareness message from ${conn.origin} total ${doc.awareness.states.size}`)
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
doc.emit('error' as any, [err]);
|
||||
}
|
||||
};
|
||||
|
||||
let contentInitializor: (ydoc: Y.Doc) => Promise<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>) => {
|
||||
contentInitializor = f;
|
||||
contentInitializor = f;
|
||||
};
|
||||
|
||||
export class WSSharedDoc extends Y.Doc {
|
||||
name: string;
|
||||
conns: Map<WebSocket, Set<number>>;
|
||||
awareness: Awareness;
|
||||
whenInitialized: Promise<void>;
|
||||
name: string;
|
||||
conns: Map<WebSocket, Set<number>>;
|
||||
awareness: Awareness;
|
||||
whenInitialized: Promise<void>;
|
||||
|
||||
constructor(name: string, gc: boolean) {
|
||||
super({ gc });
|
||||
constructor(name: string, gc: boolean) {
|
||||
super({ gc });
|
||||
|
||||
this.name = name;
|
||||
this.conns = new Map();
|
||||
this.awareness = new Awareness(this);
|
||||
this.awareness.setLocalState(null);
|
||||
this.name = name;
|
||||
this.conns = new Map();
|
||||
this.awareness = new Awareness(this);
|
||||
this.awareness.setLocalState(null);
|
||||
|
||||
const awarenessUpdateHandler = ({
|
||||
added,
|
||||
updated,
|
||||
removed
|
||||
}: {
|
||||
added: number[],
|
||||
updated: number[],
|
||||
removed: number[]
|
||||
}, conn: WebSocket) => {
|
||||
const changedClients = added.concat(updated, removed);
|
||||
if (changedClients.length === 0) return
|
||||
if (conn !== null) {
|
||||
const connControlledIDs = this.conns.get(conn) as Set<number>;
|
||||
if (connControlledIDs !== undefined) {
|
||||
added.forEach(clientID => { connControlledIDs.add(clientID); });
|
||||
removed.forEach(clientID => { connControlledIDs.delete(clientID); });
|
||||
}
|
||||
}
|
||||
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
encodeAwarenessUpdate(this.awareness, changedClients)
|
||||
);
|
||||
const buff = encoding.toUint8Array(encoder);
|
||||
|
||||
this.conns.forEach((_, c) => {
|
||||
send(this, c, buff);
|
||||
});
|
||||
};
|
||||
|
||||
this.awareness.on('update', awarenessUpdateHandler);
|
||||
this.on('update', updateHandler as any);
|
||||
|
||||
if (isCallbackSet) {
|
||||
this.on('update', debounce(
|
||||
callbackHandler as any,
|
||||
CALLBACK_DEBOUNCE_WAIT,
|
||||
{ maxWait: CALLBACK_DEBOUNCE_MAXWAIT }
|
||||
) as any);
|
||||
const awarenessUpdateHandler = (
|
||||
{
|
||||
added,
|
||||
updated,
|
||||
removed,
|
||||
}: {
|
||||
added: number[];
|
||||
updated: number[];
|
||||
removed: number[];
|
||||
},
|
||||
conn: WebSocket,
|
||||
) => {
|
||||
const changedClients = added.concat(updated, removed);
|
||||
if (changedClients.length === 0) return;
|
||||
if (conn !== null) {
|
||||
const connControlledIDs = this.conns.get(conn) as Set<number>;
|
||||
if (connControlledIDs !== undefined) {
|
||||
added.forEach((clientID) => {
|
||||
connControlledIDs.add(clientID);
|
||||
});
|
||||
removed.forEach((clientID) => {
|
||||
connControlledIDs.delete(clientID);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.whenInitialized = contentInitializor(this);
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
encodeAwarenessUpdate(this.awareness, changedClients),
|
||||
);
|
||||
const buff = encoding.toUint8Array(encoder);
|
||||
|
||||
this.conns.forEach((_, c) => {
|
||||
send(this, c, buff);
|
||||
});
|
||||
};
|
||||
|
||||
this.awareness.on('update', awarenessUpdateHandler);
|
||||
this.on('update', updateHandler as any);
|
||||
|
||||
if (isCallbackSet) {
|
||||
this.on(
|
||||
'update',
|
||||
debounce(callbackHandler as any, CALLBACK_DEBOUNCE_WAIT, {
|
||||
maxWait: CALLBACK_DEBOUNCE_MAXWAIT,
|
||||
}) as any,
|
||||
);
|
||||
}
|
||||
|
||||
this.whenInitialized = contentInitializor(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,85 +1,117 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { WebSocketType, WSClient } from "../types";
|
||||
import { BaseWebSocketServer } from "../base/base-websocket-server";
|
||||
import { encoding } from "lib0";
|
||||
import { YMessageType, writeSyncStep1, encodeAwarenessUpdate } from "@nice/common";
|
||||
import { getYDoc, closeConn, WSSharedDoc, messageListener, send } from "./ws-shared-doc";
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { WebSocketType, WSClient } from '../types';
|
||||
import { BaseWebSocketServer } from '../base/base-websocket-server';
|
||||
import { encoding } from 'lib0';
|
||||
import {
|
||||
YMessageType,
|
||||
writeSyncStep1,
|
||||
encodeAwarenessUpdate,
|
||||
} from '@nice/common';
|
||||
import {
|
||||
getYDoc,
|
||||
closeConn,
|
||||
WSSharedDoc,
|
||||
messageListener,
|
||||
send,
|
||||
} from './ws-shared-doc';
|
||||
@Injectable()
|
||||
export class YjsServer extends BaseWebSocketServer {
|
||||
public get serverType(): WebSocketType {
|
||||
return WebSocketType.YJS;
|
||||
}
|
||||
public override handleConnection(
|
||||
connection: WSClient
|
||||
): void {
|
||||
super.handleConnection(connection)
|
||||
try {
|
||||
connection.binaryType = 'arraybuffer';
|
||||
const doc = this.initializeDocument(connection, connection.roomId, true);
|
||||
this.setupConnectionHandlers(connection, doc);
|
||||
this.sendInitialSync(connection, doc);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error in handleNewConnection: ${error.message}`, error.stack);
|
||||
connection.close();
|
||||
}
|
||||
public get serverType(): WebSocketType {
|
||||
return WebSocketType.YJS;
|
||||
}
|
||||
public override handleConnection(connection: WSClient): void {
|
||||
super.handleConnection(connection);
|
||||
try {
|
||||
connection.binaryType = 'arraybuffer';
|
||||
const doc = this.initializeDocument(connection, connection.roomId, true);
|
||||
this.setupConnectionHandlers(connection, doc);
|
||||
this.sendInitialSync(connection, doc);
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Error in handleNewConnection: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
private initializeDocument(conn: WSClient, docName: string, gc: boolean) {
|
||||
const doc = getYDoc(docName, gc);
|
||||
private initializeDocument(conn: WSClient, docName: string, gc: boolean) {
|
||||
const doc = getYDoc(docName, gc);
|
||||
|
||||
doc.conns.set(conn, new Set());
|
||||
return doc;
|
||||
}
|
||||
doc.conns.set(conn, new Set());
|
||||
return doc;
|
||||
}
|
||||
|
||||
private setupConnectionHandlers(connection: WSClient, doc: WSSharedDoc): void {
|
||||
connection.on('message', (message: ArrayBuffer) => {
|
||||
this.handleMessage(connection, doc, message);
|
||||
});
|
||||
connection.on('close', () => {
|
||||
this.handleClose(doc, connection);
|
||||
});
|
||||
connection.on('error', (error) => {
|
||||
this.logger.error(`WebSocket error for doc ${doc.name}: ${error.message}`, error.stack);
|
||||
closeConn(doc, connection);
|
||||
this.logger.warn(`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`);
|
||||
});
|
||||
}
|
||||
private setupConnectionHandlers(
|
||||
connection: WSClient,
|
||||
doc: WSSharedDoc,
|
||||
): void {
|
||||
connection.on('message', (message: ArrayBuffer) => {
|
||||
this.handleMessage(connection, doc, message);
|
||||
});
|
||||
connection.on('close', () => {
|
||||
this.handleClose(doc, connection);
|
||||
});
|
||||
connection.on('error', (error) => {
|
||||
this.logger.error(
|
||||
`WebSocket error for doc ${doc.name}: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
closeConn(doc, connection);
|
||||
this.logger.warn(
|
||||
`Connection closed due to error for doc: ${doc.name}. Remaining connections: ${doc.conns.size}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private handleClose(doc: WSSharedDoc, connection: WSClient): void {
|
||||
try {
|
||||
closeConn(doc, connection);
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error closing connection: ${error.message}`, error.stack);
|
||||
}
|
||||
private handleClose(doc: WSSharedDoc, connection: WSClient): void {
|
||||
try {
|
||||
closeConn(doc, connection);
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Error closing connection: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
}
|
||||
private handleMessage(connection: WSClient, doc: WSSharedDoc, message: ArrayBuffer): void {
|
||||
try {
|
||||
messageListener(connection, doc, new Uint8Array(message));
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error handling message: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
private handleMessage(
|
||||
connection: WSClient,
|
||||
doc: WSSharedDoc,
|
||||
message: ArrayBuffer,
|
||||
): void {
|
||||
try {
|
||||
messageListener(connection, doc, new Uint8Array(message));
|
||||
} catch (error: any) {
|
||||
this.logger.error(
|
||||
`Error handling message: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
}
|
||||
private sendInitialSync(connection: WSClient, doc: any): void {
|
||||
this.sendSyncStep1(connection, doc);
|
||||
this.sendAwarenessStates(connection, doc);
|
||||
}
|
||||
private sendSyncStep1(connection: WSClient, doc: any): void {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, YMessageType.Sync);
|
||||
writeSyncStep1(encoder, doc);
|
||||
send(doc, connection, encoding.toUint8Array(encoder));
|
||||
}
|
||||
private sendAwarenessStates(connection: WSClient, doc: WSSharedDoc): void {
|
||||
const awarenessStates = doc.awareness.getStates();
|
||||
}
|
||||
private sendInitialSync(connection: WSClient, doc: any): void {
|
||||
this.sendSyncStep1(connection, doc);
|
||||
this.sendAwarenessStates(connection, doc);
|
||||
}
|
||||
private sendSyncStep1(connection: WSClient, doc: any): void {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, YMessageType.Sync);
|
||||
writeSyncStep1(encoder, doc);
|
||||
send(doc, connection, encoding.toUint8Array(encoder));
|
||||
}
|
||||
private sendAwarenessStates(connection: WSClient, doc: WSSharedDoc): void {
|
||||
const awarenessStates = doc.awareness.getStates();
|
||||
|
||||
if (awarenessStates.size > 0) {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
encodeAwarenessUpdate(doc.awareness, Array.from(awarenessStates.keys()))
|
||||
);
|
||||
send(doc, connection, encoding.toUint8Array(encoder));
|
||||
}
|
||||
if (awarenessStates.size > 0) {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, YMessageType.Awareness);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
encodeAwarenessUpdate(
|
||||
doc.awareness,
|
||||
Array.from(awarenessStates.keys()),
|
||||
),
|
||||
);
|
||||
send(doc, connection, encoding.toUint8Array(encoder));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { RealtimeServer } from './realtime.server';
|
||||
|
||||
|
||||
@Module({
|
||||
providers: [ RealtimeServer],
|
||||
exports: [ RealtimeServer]
|
||||
providers: [RealtimeServer],
|
||||
exports: [RealtimeServer],
|
||||
})
|
||||
export class RealTimeModule { }
|
||||
export class RealTimeModule {}
|
||||
|
|
|
@ -1,25 +1,37 @@
|
|||
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||
import { WebSocketType } from "../types";
|
||||
import { BaseWebSocketServer } from "../base/base-websocket-server";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nice/common";
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { WebSocketType } from '../types';
|
||||
import { BaseWebSocketServer } from '../base/base-websocket-server';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
import {
|
||||
ObjectType,
|
||||
SocketMsgType,
|
||||
MessageDto,
|
||||
PostDto,
|
||||
PostType,
|
||||
} from '@nice/common';
|
||||
@Injectable()
|
||||
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit {
|
||||
onModuleInit() {
|
||||
EventBus.on("dataChanged", ({ data, type, operation }) => {
|
||||
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
||||
const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id)
|
||||
this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } })
|
||||
}
|
||||
export class RealtimeServer
|
||||
extends BaseWebSocketServer
|
||||
implements OnModuleInit
|
||||
{
|
||||
onModuleInit() {
|
||||
EventBus.on('dataChanged', ({ data, type, operation }) => {
|
||||
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
||||
const receiverIds = (data as Partial<MessageDto>).receivers.map(
|
||||
(receiver) => receiver.id,
|
||||
);
|
||||
this.sendToUsers(receiverIds, {
|
||||
type: SocketMsgType.NOTIFY,
|
||||
payload: { objectType: ObjectType.MESSAGE },
|
||||
});
|
||||
}
|
||||
|
||||
if (type === ObjectType.POST) {
|
||||
const post = data as Partial<PostDto>
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
public get serverType(): WebSocketType {
|
||||
return WebSocketType.REALTIME;
|
||||
}
|
||||
if (type === ObjectType.POST) {
|
||||
const post = data as Partial<PostDto>;
|
||||
}
|
||||
});
|
||||
}
|
||||
public get serverType(): WebSocketType {
|
||||
return WebSocketType.REALTIME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
import { WebSocketServer, WebSocket } from "ws";
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
|
||||
// 类型定义
|
||||
export enum WebSocketType {
|
||||
YJS = "yjs",
|
||||
REALTIME = "realtime"
|
||||
YJS = 'yjs',
|
||||
REALTIME = 'realtime',
|
||||
}
|
||||
|
||||
export interface WebSocketServerConfig {
|
||||
path?: string;
|
||||
pingInterval?: number;
|
||||
pingTimeout?: number;
|
||||
debug?: boolean
|
||||
path?: string;
|
||||
pingInterval?: number;
|
||||
pingTimeout?: number;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface ServerInstance {
|
||||
wss: WebSocketServer | null;
|
||||
clients: Set<WSClient>;
|
||||
pingIntervalId?: NodeJS.Timeout;
|
||||
timeouts: Map<WebSocket, NodeJS.Timeout>;
|
||||
wss: WebSocketServer | null;
|
||||
clients: Set<WSClient>;
|
||||
pingIntervalId?: NodeJS.Timeout;
|
||||
timeouts: Map<WebSocket, NodeJS.Timeout>;
|
||||
}
|
||||
|
||||
export interface WSClient extends WebSocket {
|
||||
isAlive?: boolean;
|
||||
type?: WebSocketType;
|
||||
userId?: string
|
||||
origin?: string
|
||||
roomId?: string
|
||||
}
|
||||
isAlive?: boolean;
|
||||
type?: WebSocketType;
|
||||
userId?: string;
|
||||
origin?: string;
|
||||
roomId?: string;
|
||||
}
|
||||
|
|
|
@ -8,4 +8,4 @@ import { CollaborationModule } from './collaboration/collaboration.module';
|
|||
providers: [WebSocketService],
|
||||
exports: [WebSocketService],
|
||||
})
|
||||
export class WebSocketModule { }
|
||||
export class WebSocketModule {}
|
||||
|
|
|
@ -1,61 +1,61 @@
|
|||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Server } from "http";
|
||||
import { WSClient } from "./types";
|
||||
import { RealtimeServer } from "./realtime/realtime.server";
|
||||
import { YjsServer } from "./collaboration/yjs.server";
|
||||
import { BaseWebSocketServer } from "./base/base-websocket-server";
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Server } from 'http';
|
||||
import { WSClient } from './types';
|
||||
import { RealtimeServer } from './realtime/realtime.server';
|
||||
import { YjsServer } from './collaboration/yjs.server';
|
||||
import { BaseWebSocketServer } from './base/base-websocket-server';
|
||||
|
||||
@Injectable()
|
||||
export class WebSocketService {
|
||||
private readonly logger = new Logger(WebSocketService.name);
|
||||
private readonly servers: BaseWebSocketServer[] = [];
|
||||
constructor(
|
||||
private realTimeServer: RealtimeServer,
|
||||
private yjsServer: YjsServer
|
||||
) {
|
||||
this.servers.push(this.realTimeServer)
|
||||
this.servers.push(this.yjsServer)
|
||||
private readonly logger = new Logger(WebSocketService.name);
|
||||
private readonly servers: BaseWebSocketServer[] = [];
|
||||
constructor(
|
||||
private realTimeServer: RealtimeServer,
|
||||
private yjsServer: YjsServer,
|
||||
) {
|
||||
this.servers.push(this.realTimeServer);
|
||||
this.servers.push(this.yjsServer);
|
||||
}
|
||||
public async initialize(httpServer: Server): Promise<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 {
|
||||
await Promise.all(this.servers.map(server => server.start()));
|
||||
this.setupUpgradeHandler(httpServer);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to initialize:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
private setupUpgradeHandler(httpServer: Server): void {
|
||||
if (httpServer.listeners('upgrade').length) return;
|
||||
httpServer.on('upgrade', async (request, socket, head) => {
|
||||
try {
|
||||
const url = new URL(request.url!, `http://${request.headers.host}`);
|
||||
const pathname = url.pathname;
|
||||
}
|
||||
private setupUpgradeHandler(httpServer: Server): void {
|
||||
if (httpServer.listeners('upgrade').length) return;
|
||||
httpServer.on('upgrade', async (request, socket, head) => {
|
||||
try {
|
||||
const url = new URL(request.url!, `http://${request.headers.host}`);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// 从URL查询参数中获取roomId和token
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
const roomId = urlParams.get('roomId');
|
||||
const userId = urlParams.get('userId');
|
||||
const server = this.servers.find(server => {
|
||||
const serverPathClean = server.serverPath.replace(/\/$/, '');
|
||||
const pathnameClean = pathname.replace(/\/$/, '');
|
||||
return serverPathClean === pathnameClean;
|
||||
});
|
||||
|
||||
if (!server || !server.wss) {
|
||||
return socket.destroy();
|
||||
}
|
||||
|
||||
server.wss!.handleUpgrade(request, socket, head, (ws: WSClient) => {
|
||||
ws.userId = userId;
|
||||
ws.origin = request.url
|
||||
ws.roomId = roomId
|
||||
server.wss!.emit('connection', ws, request);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Upgrade error:', error);
|
||||
socket.destroy();
|
||||
}
|
||||
// 从URL查询参数中获取roomId和token
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
const roomId = urlParams.get('roomId');
|
||||
const userId = urlParams.get('userId');
|
||||
const server = this.servers.find((server) => {
|
||||
const serverPathClean = server.serverPath.replace(/\/$/, '');
|
||||
const pathnameClean = pathname.replace(/\/$/, '');
|
||||
return serverPathClean === pathnameClean;
|
||||
});
|
||||
}
|
||||
|
||||
if (!server || !server.wss) {
|
||||
return socket.destroy();
|
||||
}
|
||||
|
||||
server.wss!.handleUpgrade(request, socket, head, (ws: WSClient) => {
|
||||
ws.userId = userId;
|
||||
ws.origin = request.url;
|
||||
ws.roomId = roomId;
|
||||
server.wss!.emit('connection', ws, request);
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error('Upgrade error:', error);
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,8 +52,8 @@ export class GenDevService {
|
|||
await this.generateStaffs(4);
|
||||
//await this.generateTerms(2, 6);
|
||||
//await this.generateCourses(8);
|
||||
await this.generateTrainContent(2,6)
|
||||
await this.generateTrainSituations()
|
||||
await this.generateTrainContent(2, 6);
|
||||
await this.generateTrainSituations();
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
}
|
||||
|
@ -88,7 +88,9 @@ export class GenDevService {
|
|||
this.domains.forEach((domain) => {
|
||||
this.domainDepts[domain.id] = this.getAllChildDepartments(domain.id);
|
||||
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.`);
|
||||
|
@ -104,7 +106,9 @@ export class GenDevService {
|
|||
if (currentDepth > maxDepth) return;
|
||||
|
||||
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(
|
||||
deptName,
|
||||
parentId,
|
||||
|
@ -190,7 +194,9 @@ export class GenDevService {
|
|||
});
|
||||
for (const cate of cates) {
|
||||
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 =
|
||||
levels[Math.floor(Math.random() * levels.length)].id;
|
||||
const randomDeptId =
|
||||
|
@ -224,7 +230,9 @@ export class GenDevService {
|
|||
this.deptStaffRecord[dept.id] = [];
|
||||
}
|
||||
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({
|
||||
data: {
|
||||
showname: username,
|
||||
|
@ -328,7 +336,9 @@ export class GenDevService {
|
|||
) => {
|
||||
if (currentDepth > depth) return;
|
||||
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({
|
||||
data: {
|
||||
name,
|
||||
|
@ -347,106 +357,113 @@ export class GenDevService {
|
|||
}
|
||||
// 生成培训内容
|
||||
private async createTrainContent(
|
||||
type:string,
|
||||
title:string,
|
||||
parentId:string|null
|
||||
){
|
||||
type: string,
|
||||
title: string,
|
||||
parentId: string | null,
|
||||
) {
|
||||
const trainContent = await db.trainContent.create({
|
||||
data: {
|
||||
type,
|
||||
title,
|
||||
parentId
|
||||
parentId,
|
||||
},
|
||||
});
|
||||
return trainContent;
|
||||
}
|
||||
// 生成培训内容
|
||||
private async generateTrainContent(depth:number=3,count:number=6){
|
||||
if(this.counts.trainContentCount !== 0) return;
|
||||
const totalTrainContent = this.calculateTotalTrainContent(depth,count)
|
||||
this.logger.log("Start generating train content...")
|
||||
await this.generateSubTrainContent(null,1,depth,count,totalTrainContent)
|
||||
this.trainContents = await db.trainContent.findMany()
|
||||
this.logger.log(`Completed: Generated ${this.trainContents.length} departments.`);
|
||||
private async generateTrainContent(depth: number = 3, count: number = 6) {
|
||||
if (this.counts.trainContentCount !== 0) return;
|
||||
const totalTrainContent = this.calculateTotalTrainContent(depth, count);
|
||||
this.logger.log('Start generating train content...');
|
||||
await this.generateSubTrainContent(
|
||||
null,
|
||||
1,
|
||||
depth,
|
||||
count,
|
||||
totalTrainContent,
|
||||
);
|
||||
this.trainContents = await db.trainContent.findMany();
|
||||
this.logger.log(
|
||||
`Completed: Generated ${this.trainContents.length} departments.`,
|
||||
);
|
||||
}
|
||||
// 生成培训内容子内容
|
||||
private async generateSubTrainContent(
|
||||
parentId: string | null,
|
||||
currentDepth:number,
|
||||
maxDepth:number,
|
||||
count:number,
|
||||
total:number,
|
||||
){
|
||||
if(currentDepth > maxDepth) return;
|
||||
const contentType = [TrainContentType.SUBJECTS,TrainContentType.COURSE]
|
||||
for(let i = 0 ; i < count ; i++){
|
||||
const trainContentTitle = `${parentId?.slice(0,6) || '根'}公司${currentDepth}-${i}`
|
||||
currentDepth: number,
|
||||
maxDepth: number,
|
||||
count: number,
|
||||
total: number,
|
||||
) {
|
||||
if (currentDepth > maxDepth) return;
|
||||
const contentType = [TrainContentType.SUBJECTS, TrainContentType.COURSE];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const trainContentTitle = `${
|
||||
parentId?.slice(0, 6) || '根'
|
||||
}公司${currentDepth}-${i}`;
|
||||
const newTrainContent = await this.createTrainContent(
|
||||
contentType[currentDepth-1],
|
||||
contentType[currentDepth - 1],
|
||||
trainContentTitle,
|
||||
parentId
|
||||
)
|
||||
parentId,
|
||||
);
|
||||
this.trainContentGeneratedCount++;
|
||||
this.logger.log(
|
||||
`Generated ${this.trainContentGeneratedCount}/${total} train contents`
|
||||
)
|
||||
`Generated ${this.trainContentGeneratedCount}/${total} train contents`,
|
||||
);
|
||||
await this.generateSubTrainContent(
|
||||
newTrainContent.id,
|
||||
currentDepth+1,
|
||||
currentDepth + 1,
|
||||
maxDepth,
|
||||
count,
|
||||
total
|
||||
)
|
||||
total,
|
||||
);
|
||||
}
|
||||
}
|
||||
private calculateTotalTrainContent(depth:number,count:number):number{
|
||||
private calculateTotalTrainContent(depth: number, count: number): number {
|
||||
let total = 0;
|
||||
for(let i = 1 ; i<=depth;i++){
|
||||
total += Math.pow(count,i);
|
||||
for (let i = 1; i <= depth; i++) {
|
||||
total += Math.pow(count, i);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
|
||||
private async createTrainSituation(staffId:string,trainContentId:string){
|
||||
private async createTrainSituation(staffId: string, trainContentId: string) {
|
||||
const trainSituation = await db.trainSituation.create({
|
||||
data:{
|
||||
data: {
|
||||
staffId,
|
||||
trainContentId,
|
||||
mustTrainTime:Math.floor(Math.random()*100),
|
||||
alreadyTrainTime:Math.floor(Math.random()*100),
|
||||
score:Math.floor(Math.random()*100),
|
||||
}
|
||||
})
|
||||
return trainSituation
|
||||
mustTrainTime: Math.floor(Math.random() * 100),
|
||||
alreadyTrainTime: Math.floor(Math.random() * 100),
|
||||
score: Math.floor(Math.random() * 100),
|
||||
},
|
||||
});
|
||||
return trainSituation;
|
||||
}
|
||||
|
||||
private async generateTrainSituations(probability: number = 0.1){
|
||||
this.logger.log("Start generating train situations...")
|
||||
private async generateTrainSituations(probability: number = 0.1) {
|
||||
this.logger.log('Start generating train situations...');
|
||||
const allTrainContents = await db.trainContent.findMany();
|
||||
// 这里相当于两次遍历 找到没有parentID的即是
|
||||
const leafNodes = allTrainContents.filter((item)=>item.parentId !== null)
|
||||
console.log(leafNodes.length)
|
||||
const staffs = await db.staff.findMany()
|
||||
const leafNodes = allTrainContents.filter((item) => item.parentId !== null);
|
||||
console.log(leafNodes.length);
|
||||
const staffs = await db.staff.findMany();
|
||||
|
||||
let situationCount = 0
|
||||
const totalPossibleSituations = leafNodes.length * staffs.length
|
||||
let situationCount = 0;
|
||||
const totalPossibleSituations = leafNodes.length * staffs.length;
|
||||
|
||||
for (const staff of staffs){
|
||||
for(const leaf of leafNodes){
|
||||
if(Math.random() < probability){
|
||||
await this.createTrainSituation(staff.id,leaf.id)
|
||||
situationCount++
|
||||
for (const staff of staffs) {
|
||||
for (const leaf of leafNodes) {
|
||||
if (Math.random() < probability) {
|
||||
await this.createTrainSituation(staff.id, leaf.id);
|
||||
situationCount++;
|
||||
if (situationCount % 100 === 0) {
|
||||
this.logger.log(
|
||||
`Generated ${situationCount} train situations`
|
||||
);
|
||||
this.logger.log(`Generated ${situationCount} train situations`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.logger.log(
|
||||
`Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.`
|
||||
`Completed: Generated ${situationCount} train situations out of ${totalPossibleSituations} possible combinations.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,15 @@ import { DepartmentModule } from '@server/models/department/department.module';
|
|||
import { TermModule } from '@server/models/term/term.module';
|
||||
|
||||
@Module({
|
||||
imports: [MinioModule, AuthModule, AppConfigModule, StaffModule, DepartmentModule, TermModule],
|
||||
imports: [
|
||||
MinioModule,
|
||||
AuthModule,
|
||||
AppConfigModule,
|
||||
StaffModule,
|
||||
DepartmentModule,
|
||||
TermModule,
|
||||
],
|
||||
providers: [InitService, GenDevService],
|
||||
exports: [InitService]
|
||||
exports: [InitService],
|
||||
})
|
||||
export class InitModule { }
|
||||
export class InitModule {}
|
||||
|
|
|
@ -19,7 +19,7 @@ export class InitService {
|
|||
private readonly minioService: MinioService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly genDevService: GenDevService,
|
||||
) { }
|
||||
) {}
|
||||
private async createRoles() {
|
||||
this.logger.log('Checking existing system roles');
|
||||
for (const role of InitRoles) {
|
||||
|
@ -122,16 +122,16 @@ export class InitService {
|
|||
const existingTrainContent = await db.trainContent.findFirst({
|
||||
where: {
|
||||
type: 'SPORTS',
|
||||
title: '体能考核'
|
||||
}
|
||||
title: '体能考核',
|
||||
},
|
||||
});
|
||||
if (!existingTrainContent) {
|
||||
await db.trainContent.create({
|
||||
data: {
|
||||
type: 'SPORTS',
|
||||
title: '体能考核'
|
||||
}
|
||||
})
|
||||
title: '体能考核',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.logger.log('Sport train already exists');
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export interface DevDataCounts {
|
|||
export async function getCounts(): Promise<DevDataCounts> {
|
||||
const counts = {
|
||||
deptCount: await db.department.count(),
|
||||
trainContentCount:await db.trainContent.count(),
|
||||
trainContentCount: await db.trainContent.count(),
|
||||
staffCount: await db.staff.count(),
|
||||
termCount: await db.term.count(),
|
||||
courseCount: await db.post.count({
|
||||
|
|
|
@ -3,8 +3,8 @@ import { ReminderService } from './reminder.service';
|
|||
import { MessageModule } from '@server/models/message/message.module';
|
||||
|
||||
@Module({
|
||||
imports: [ MessageModule],
|
||||
imports: [MessageModule],
|
||||
providers: [ReminderService],
|
||||
exports: [ReminderService]
|
||||
exports: [ReminderService],
|
||||
})
|
||||
export class ReminderModule { }
|
||||
export class ReminderModule {}
|
||||
|
|
|
@ -15,67 +15,65 @@ import { MessageService } from '@server/models/message/message.service';
|
|||
*/
|
||||
@Injectable()
|
||||
export class ReminderService {
|
||||
/**
|
||||
* 日志记录器实例
|
||||
* @private
|
||||
*/
|
||||
private readonly logger = new Logger(ReminderService.name);
|
||||
/**
|
||||
* 日志记录器实例
|
||||
* @private
|
||||
*/
|
||||
private readonly logger = new Logger(ReminderService.name);
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param messageService 消息服务实例
|
||||
*/
|
||||
constructor(private readonly messageService: MessageService) { }
|
||||
/**
|
||||
* 构造函数
|
||||
* @param messageService 消息服务实例
|
||||
*/
|
||||
constructor(private readonly messageService: MessageService) {}
|
||||
|
||||
/**
|
||||
* 生成提醒时间点
|
||||
* @param totalDays 总天数
|
||||
* @returns 提醒时间点数组
|
||||
*/
|
||||
generateReminderTimes(totalDays: number): number[] {
|
||||
// 如果总天数小于3天则不需要提醒
|
||||
if (totalDays < 3) return [];
|
||||
// 使用Set存储提醒时间点,避免重复
|
||||
const reminders: Set<number> = new Set();
|
||||
// 按照2的幂次方划分时间点
|
||||
for (let i = 1; i <= totalDays / 2; i++) {
|
||||
reminders.add(Math.ceil(totalDays / Math.pow(2, i)));
|
||||
}
|
||||
// 将Set转为数组并升序排序
|
||||
return Array.from(reminders).sort((a, b) => a - b);
|
||||
/**
|
||||
* 生成提醒时间点
|
||||
* @param totalDays 总天数
|
||||
* @returns 提醒时间点数组
|
||||
*/
|
||||
generateReminderTimes(totalDays: number): number[] {
|
||||
// 如果总天数小于3天则不需要提醒
|
||||
if (totalDays < 3) return [];
|
||||
// 使用Set存储提醒时间点,避免重复
|
||||
const reminders: Set<number> = new Set();
|
||||
// 按照2的幂次方划分时间点
|
||||
for (let i = 1; i <= totalDays / 2; i++) {
|
||||
reminders.add(Math.ceil(totalDays / Math.pow(2, i)));
|
||||
}
|
||||
// 将Set转为数组并升序排序
|
||||
return Array.from(reminders).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要发送提醒
|
||||
* @param createdAt 创建时间
|
||||
* @param deadline 截止时间
|
||||
* @returns 是否需要提醒及剩余天数
|
||||
*/
|
||||
shouldSendReminder(createdAt: Date, deadline: Date) {
|
||||
// 获取当前时间
|
||||
const now = dayjs();
|
||||
const end = dayjs(deadline);
|
||||
// 计算总时间和剩余时间(天)
|
||||
const totalTimeDays = end.diff(createdAt, 'day');
|
||||
const timeLeftDays = end.diff(now, 'day');
|
||||
/**
|
||||
* 判断是否需要发送提醒
|
||||
* @param createdAt 创建时间
|
||||
* @param deadline 截止时间
|
||||
* @returns 是否需要提醒及剩余天数
|
||||
*/
|
||||
shouldSendReminder(createdAt: Date, deadline: Date) {
|
||||
// 获取当前时间
|
||||
const now = dayjs();
|
||||
const end = dayjs(deadline);
|
||||
// 计算总时间和剩余时间(天)
|
||||
const totalTimeDays = end.diff(createdAt, 'day');
|
||||
const timeLeftDays = end.diff(now, 'day');
|
||||
|
||||
if (totalTimeDays > 1) {
|
||||
// 获取提醒时间点
|
||||
const reminderTimes = this.generateReminderTimes(totalTimeDays);
|
||||
// 如果剩余时间在提醒时间点内,则需要提醒
|
||||
if (reminderTimes.includes(timeLeftDays)) {
|
||||
return { shouldSend: true, timeLeft: timeLeftDays };
|
||||
}
|
||||
}
|
||||
return { shouldSend: false, timeLeft: timeLeftDays };
|
||||
if (totalTimeDays > 1) {
|
||||
// 获取提醒时间点
|
||||
const reminderTimes = this.generateReminderTimes(totalTimeDays);
|
||||
// 如果剩余时间在提醒时间点内,则需要提醒
|
||||
if (reminderTimes.includes(timeLeftDays)) {
|
||||
return { shouldSend: true, timeLeft: timeLeftDays };
|
||||
}
|
||||
}
|
||||
return { shouldSend: false, timeLeft: timeLeftDays };
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送截止日期提醒
|
||||
*/
|
||||
async remindDeadline() {
|
||||
this.logger.log('开始检查截止日期以发送提醒。');
|
||||
|
||||
|
||||
}
|
||||
/**
|
||||
* 发送截止日期提醒
|
||||
*/
|
||||
async remindDeadline() {
|
||||
this.logger.log('开始检查截止日期以发送提醒。');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TasksService } from './tasks.service';
|
||||
import { InitModule } from '@server/tasks/init/init.module';
|
||||
import { ReminderModule } from "@server/tasks/reminder/reminder.module"
|
||||
import { ReminderModule } from '@server/tasks/reminder/reminder.module';
|
||||
@Module({
|
||||
imports: [InitModule, ReminderModule],
|
||||
providers: [TasksService]
|
||||
providers: [TasksService],
|
||||
})
|
||||
export class TasksModule { }
|
||||
export class TasksModule {}
|
||||
|
|
|
@ -6,41 +6,47 @@ import { CronJob } from 'cron';
|
|||
|
||||
@Injectable()
|
||||
export class TasksService implements OnModuleInit {
|
||||
private readonly logger = new Logger(TasksService.name);
|
||||
private readonly logger = new Logger(TasksService.name);
|
||||
|
||||
constructor(
|
||||
private readonly schedulerRegistry: SchedulerRegistry,
|
||||
private readonly initService: InitService,
|
||||
private readonly reminderService: ReminderService
|
||||
) { }
|
||||
constructor(
|
||||
private readonly schedulerRegistry: SchedulerRegistry,
|
||||
private readonly initService: InitService,
|
||||
private readonly reminderService: ReminderService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
this.logger.log('Main node launch');
|
||||
await this.initService.init();
|
||||
this.logger.log('Initialization successful');
|
||||
async onModuleInit() {
|
||||
this.logger.log('Main node launch');
|
||||
await this.initService.init();
|
||||
this.logger.log('Initialization successful');
|
||||
|
||||
try {
|
||||
const cronExpression = process.env.DEADLINE_CRON;
|
||||
if (!cronExpression) {
|
||||
throw new Error('DEADLINE_CRON environment variable is not set');
|
||||
}
|
||||
|
||||
const handleRemindJob = new CronJob(cronExpression, async () => {
|
||||
try {
|
||||
const cronExpression = process.env.DEADLINE_CRON;
|
||||
if (!cronExpression) {
|
||||
throw new Error('DEADLINE_CRON environment variable is not set');
|
||||
}
|
||||
|
||||
const handleRemindJob = new CronJob(cronExpression, async () => {
|
||||
try {
|
||||
await this.reminderService.remindDeadline();
|
||||
this.logger.log('Reminder successfully processed');
|
||||
} catch (reminderErr) {
|
||||
this.logger.error('Error occurred while processing reminder', reminderErr);
|
||||
}
|
||||
});
|
||||
|
||||
this.schedulerRegistry.addCronJob('remindDeadline', handleRemindJob as any);
|
||||
this.logger.log('Start remind cron job');
|
||||
handleRemindJob.start();
|
||||
} catch (cronJobErr) {
|
||||
this.logger.error('Failed to initialize cron job', cronJobErr);
|
||||
// Optionally rethrow the error if you want to halt further execution
|
||||
// throw cronJobErr;
|
||||
await this.reminderService.remindDeadline();
|
||||
this.logger.log('Reminder successfully processed');
|
||||
} catch (reminderErr) {
|
||||
this.logger.error(
|
||||
'Error occurred while processing reminder',
|
||||
reminderErr,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.schedulerRegistry.addCronJob(
|
||||
'remindDeadline',
|
||||
handleRemindJob as any,
|
||||
);
|
||||
this.logger.log('Start remind cron job');
|
||||
handleRemindJob.start();
|
||||
} catch (cronJobErr) {
|
||||
this.logger.error('Failed to initialize cron job', cronJobErr);
|
||||
// Optionally rethrow the error if you want to halt further execution
|
||||
// throw cronJobErr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ import { SportProjectModule } from '@server/models/sport-project/sportProject.mo
|
|||
TrainContentModule,
|
||||
TrainSituationModule,
|
||||
DailyTrainModule,
|
||||
SportProjectModule
|
||||
SportProjectModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [TrpcService, TrpcRouter, Logger],
|
||||
|
|
|
@ -37,10 +37,10 @@ export class TrpcRouter {
|
|||
private readonly visitor: VisitRouter,
|
||||
private readonly resource: ResourceRouter,
|
||||
private readonly trainContent: TrainContentRouter,
|
||||
private readonly trainSituation:TrainSituationRouter,
|
||||
private readonly dailyTrain:DailyTrainRouter,
|
||||
private readonly sportStandard:SportStandardRouter,
|
||||
private readonly sportProject:SportProjectRouter
|
||||
private readonly trainSituation: TrainSituationRouter,
|
||||
private readonly dailyTrain: DailyTrainRouter,
|
||||
private readonly sportStandard: SportStandardRouter,
|
||||
private readonly sportProject: SportProjectRouter,
|
||||
) {}
|
||||
getRouter() {
|
||||
return;
|
||||
|
@ -58,11 +58,11 @@ export class TrpcRouter {
|
|||
app_config: this.app_config.router,
|
||||
visitor: this.visitor.router,
|
||||
resource: this.resource.router,
|
||||
trainContent:this.trainContent.router,
|
||||
trainSituation:this.trainSituation.router,
|
||||
dailyTrain:this.dailyTrain.router,
|
||||
sportStandard:this.sportStandard.router,
|
||||
sportProject:this.sportProject.router
|
||||
trainContent: this.trainContent.router,
|
||||
trainSituation: this.trainSituation.router,
|
||||
dailyTrain: this.dailyTrain.router,
|
||||
sportStandard: this.sportStandard.router,
|
||||
sportProject: this.sportProject.router,
|
||||
});
|
||||
wss: WebSocketServer = undefined;
|
||||
|
||||
|
|
|
@ -18,8 +18,9 @@ export class TrpcService {
|
|||
ip: string;
|
||||
}> {
|
||||
const token = opts.req.headers.authorization?.split(' ')[1];
|
||||
const staff =
|
||||
await UserProfileService.instance.getUserProfileByToken(token);
|
||||
const staff = await UserProfileService.instance.getUserProfileByToken(
|
||||
token,
|
||||
);
|
||||
const ip = getClientIp(opts.req);
|
||||
return {
|
||||
staff: staff.staff,
|
||||
|
|
|
@ -3,6 +3,6 @@ import { MinioService } from './minio.service';
|
|||
|
||||
@Module({
|
||||
providers: [MinioService],
|
||||
exports: [MinioService]
|
||||
exports: [MinioService],
|
||||
})
|
||||
export class MinioModule {}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { redis } from "./redis.service";
|
||||
import { redis } from './redis.service';
|
||||
|
||||
export async function deleteByPattern(pattern: string) {
|
||||
try {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await redis.del(keys);
|
||||
// this.logger.log(`Deleted ${keys.length} keys matching pattern ${pattern}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete keys by pattern ${pattern}:`, error);
|
||||
try {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await redis.del(keys);
|
||||
// this.logger.log(`Deleted ${keys.length} keys matching pattern ${pattern}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete keys by pattern ${pattern}:`, error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,148 +1,149 @@
|
|||
import { createReadStream } from "fs";
|
||||
import { createInterface } from "readline";
|
||||
import { createReadStream } from 'fs';
|
||||
import { createInterface } from 'readline';
|
||||
|
||||
import { db } from '@nice/common';
|
||||
import * as tus from "tus-js-client";
|
||||
import { db } from '@nice/common';
|
||||
import * as tus from 'tus-js-client';
|
||||
import ExcelJS from 'exceljs';
|
||||
|
||||
export function truncateStringByByte(str, maxBytes) {
|
||||
let byteCount = 0;
|
||||
let index = 0;
|
||||
while (index < str.length && byteCount + new TextEncoder().encode(str[index]).length <= maxBytes) {
|
||||
byteCount += new TextEncoder().encode(str[index]).length;
|
||||
index++;
|
||||
}
|
||||
return str.substring(0, index) + (index < str.length ? "..." : "");
|
||||
let byteCount = 0;
|
||||
let index = 0;
|
||||
while (
|
||||
index < str.length &&
|
||||
byteCount + new TextEncoder().encode(str[index]).length <= maxBytes
|
||||
) {
|
||||
byteCount += new TextEncoder().encode(str[index]).length;
|
||||
index++;
|
||||
}
|
||||
return str.substring(0, index) + (index < str.length ? '...' : '');
|
||||
}
|
||||
export async function loadPoliciesFromCSV(filePath: string) {
|
||||
const policies = {
|
||||
p: [],
|
||||
g: []
|
||||
};
|
||||
const stream = createReadStream(filePath);
|
||||
const rl = createInterface({
|
||||
input: stream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
const policies = {
|
||||
p: [],
|
||||
g: [],
|
||||
};
|
||||
const stream = createReadStream(filePath);
|
||||
const rl = createInterface({
|
||||
input: stream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
// Updated regex to handle commas inside parentheses as part of a single field
|
||||
const regex = /(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g;
|
||||
// Updated regex to handle commas inside parentheses as part of a single field
|
||||
const regex =
|
||||
/(?:\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)|"(?:\\"|[^"])*"|[^,"()\s]+)(?=\s*,|\s*$)/g;
|
||||
|
||||
for await (const line of rl) {
|
||||
// Ignore empty lines and comments
|
||||
if (line.trim() && !line.startsWith("#")) {
|
||||
const parts = [];
|
||||
let match;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
// Remove quotes if present and trim whitespace
|
||||
parts.push(match[0].replace(/^"|"$/g, '').trim());
|
||||
}
|
||||
for await (const line of rl) {
|
||||
// Ignore empty lines and comments
|
||||
if (line.trim() && !line.startsWith('#')) {
|
||||
const parts = [];
|
||||
let match;
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
// Remove quotes if present and trim whitespace
|
||||
parts.push(match[0].replace(/^"|"$/g, '').trim());
|
||||
}
|
||||
|
||||
// Check policy type (p or g)
|
||||
const ptype = parts[0];
|
||||
const rule = parts.slice(1);
|
||||
// Check policy type (p or g)
|
||||
const ptype = parts[0];
|
||||
const rule = parts.slice(1);
|
||||
|
||||
if (ptype === 'p' || ptype === 'g') {
|
||||
policies[ptype].push(rule);
|
||||
} else {
|
||||
console.warn(`Unknown policy type '${ptype}' in policy: ${line}`);
|
||||
}
|
||||
}
|
||||
if (ptype === 'p' || ptype === 'g') {
|
||||
policies[ptype].push(rule);
|
||||
} else {
|
||||
console.warn(`Unknown policy type '${ptype}' in policy: ${line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return policies;
|
||||
return policies;
|
||||
}
|
||||
|
||||
export function uploadFile(blob: any, fileName: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const upload = new tus.Upload(blob, {
|
||||
endpoint: `${process.env.TUS_URL}/files/`,
|
||||
retryDelays: [0, 1000, 3000, 5000],
|
||||
metadata: {
|
||||
filename: fileName,
|
||||
filetype:
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed because: " + error);
|
||||
reject(error); // 错误时,我们要拒绝 promise
|
||||
},
|
||||
onProgress: (bytesUploaded, bytesTotal) => {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
// console.log(bytesUploaded, bytesTotal, `${percentage}%`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// console.log('Upload finished:', upload.url);
|
||||
resolve(upload.url); // 成功后,我们解析 promise,并返回上传的 URL
|
||||
},
|
||||
});
|
||||
upload.start();
|
||||
return new Promise((resolve, reject) => {
|
||||
const upload = new tus.Upload(blob, {
|
||||
endpoint: `${process.env.TUS_URL}/files/`,
|
||||
retryDelays: [0, 1000, 3000, 5000],
|
||||
metadata: {
|
||||
filename: fileName,
|
||||
filetype:
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Failed because: ' + error);
|
||||
reject(error); // 错误时,我们要拒绝 promise
|
||||
},
|
||||
onProgress: (bytesUploaded, bytesTotal) => {
|
||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
||||
// console.log(bytesUploaded, bytesTotal, `${percentage}%`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
// console.log('Upload finished:', upload.url);
|
||||
resolve(upload.url); // 成功后,我们解析 promise,并返回上传的 URL
|
||||
},
|
||||
});
|
||||
upload.start();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class TreeNode {
|
||||
value: string;
|
||||
children: TreeNode[];
|
||||
|
||||
constructor(value: string) {
|
||||
this.value = value;
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
addChild(childValue: string): TreeNode {
|
||||
let newChild = undefined
|
||||
if (this.children.findIndex(child => child.value === childValue) === -1) {
|
||||
newChild = new TreeNode(childValue);
|
||||
this.children.push(newChild)
|
||||
|
||||
}
|
||||
return this.children.find(child => child.value === childValue)
|
||||
value: string;
|
||||
children: TreeNode[];
|
||||
|
||||
constructor(value: string) {
|
||||
this.value = value;
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
addChild(childValue: string): TreeNode {
|
||||
let newChild = undefined;
|
||||
if (this.children.findIndex((child) => child.value === childValue) === -1) {
|
||||
newChild = new TreeNode(childValue);
|
||||
this.children.push(newChild);
|
||||
}
|
||||
return this.children.find((child) => child.value === childValue);
|
||||
}
|
||||
}
|
||||
function buildTree(data: string[][]): TreeNode {
|
||||
const root = new TreeNode('root');
|
||||
try {
|
||||
for (const path of data) {
|
||||
let currentNode = root;
|
||||
for (const value of path) {
|
||||
currentNode = currentNode.addChild(value);
|
||||
}
|
||||
}
|
||||
return root;
|
||||
const root = new TreeNode('root');
|
||||
try {
|
||||
for (const path of data) {
|
||||
let currentNode = root;
|
||||
for (const value of path) {
|
||||
currentNode = currentNode.addChild(value);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
|
||||
return root;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
export function printTree(node: TreeNode, level: number = 0): void {
|
||||
const indent = ' '.repeat(level);
|
||||
// console.log(`${indent}${node.value}`);
|
||||
for (const child of node.children) {
|
||||
printTree(child, level + 1);
|
||||
}
|
||||
const indent = ' '.repeat(level);
|
||||
// console.log(`${indent}${node.value}`);
|
||||
for (const child of node.children) {
|
||||
printTree(child, level + 1);
|
||||
}
|
||||
}
|
||||
export async function generateTreeFromFile(file: Buffer): Promise<TreeNode> {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file);
|
||||
const worksheet = workbook.getWorksheet(1);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.load(file);
|
||||
const worksheet = workbook.getWorksheet(1);
|
||||
|
||||
const data: string[][] = [];
|
||||
const data: string[][] = [];
|
||||
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
if (rowNumber > 1) { // Skip header row if any
|
||||
const rowData: string[] = (row.values as string[]).slice(2).map(cell => (cell || '').toString());
|
||||
data.push(rowData.map(value => value.trim()));
|
||||
}
|
||||
});
|
||||
// Fill forward values
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
for (let j = 0; j < data[i].length; j++) {
|
||||
if (!data[i][j]) data[i][j] = data[i - 1][j];
|
||||
}
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
if (rowNumber > 1) {
|
||||
// Skip header row if any
|
||||
const rowData: string[] = (row.values as string[])
|
||||
.slice(2)
|
||||
.map((cell) => (cell || '').toString());
|
||||
data.push(rowData.map((value) => value.trim()));
|
||||
}
|
||||
return buildTree(data);
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -2,17 +2,17 @@ import { PipeTransform, BadRequestException } from '@nestjs/common';
|
|||
import { ZodSchema } from 'zod';
|
||||
|
||||
export class ZodValidationPipe implements PipeTransform {
|
||||
constructor(private schema: ZodSchema) { }
|
||||
constructor(private schema: ZodSchema) {}
|
||||
|
||||
transform(value: unknown) {
|
||||
try {
|
||||
const result = this.schema.parse(value);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
throw new BadRequestException('Validation failed', {
|
||||
cause: error,
|
||||
description: error.errors
|
||||
});
|
||||
}
|
||||
transform(value: unknown) {
|
||||
try {
|
||||
const result = this.schema.parse(value);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
throw new BadRequestException('Validation failed', {
|
||||
cause: error,
|
||||
description: error.errors,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,3 +3,5 @@ VITE_APP_SERVER_PORT=3000
|
|||
VITE_APP_FILE_PORT=80
|
||||
VITE_APP_VERSION=0.3.0
|
||||
VITE_APP_APP_NAME=MOOC
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
VITE_APP_FILE_PORT: "$FILE_PORT",
|
||||
};
|
||||
</script>
|
||||
<title>$APP_NAME</title>
|
||||
<title>trainng_DATA</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -1,42 +1,77 @@
|
|||
.ag-theme-alpine {
|
||||
--ag-primary-color: var(--color-primary);
|
||||
--ag-alpine-active-color: var(--color-primary);
|
||||
--ag-background-color: var(--color-bg-container);
|
||||
--ag-foreground-color: var(--colorText);
|
||||
--ag-borders-critical: solid 1px;
|
||||
--ag-critical-border-color: var(--color-border-secondary);
|
||||
--ag-borders: 1px solid;
|
||||
--ag-borders-input: solid 1px;
|
||||
--ag-border-color: var(--color-border-secondary);
|
||||
--ag-secondary-border-color: var(--color-border-secondary);
|
||||
--ag-secondary-foreground-color: var(--color-text-tertiary);
|
||||
/* --ag-border-radius: 2px; */
|
||||
--ag-header-column-separator-display: block;
|
||||
--ag-header-column-separator-height: 30%;
|
||||
--ag-header-column-separator-width: 2px;
|
||||
--ag-header-column-separator-color: var(--color-fill-secondary);
|
||||
--ag-font-size: var(--fontSize);
|
||||
--ag-header-background-color: white;
|
||||
--ag-selected-row-background-color: var(--color-border-primary);
|
||||
--ag-range-selection-border-color: var(--color-border-primary);
|
||||
--ag-header-font-size: var(--fontSize);
|
||||
--ag-header-font-weight: 600;
|
||||
--ag-header-foreground-color: var(--color-primary);
|
||||
--ag-row-border-style: solid;
|
||||
--ag-row-border-width: 1px;
|
||||
--ag-row-border-color: var(--color-border-secondary);
|
||||
--ag-row-hover-color: var(--color-bg-text-hover);
|
||||
--ag-padding-horizontal: 0.7rem;
|
||||
--ag-padding-vertical: 0.9rem;
|
||||
--ag-side-panel-border-width: 1px;
|
||||
--ag-side-panel-border-color: var(--color-border-secondary);
|
||||
--ag-spacing: 6px;
|
||||
--ag-odd-row-background-color: var(--color-fill-quaternary);
|
||||
--ag-wrapper-border-width: 0px;
|
||||
/* --ag-wrapper-border-color: var(--color-border-secondary); */
|
||||
/* --ag-wrapper-border-radius: 10px; */
|
||||
--ag-primary-color: var(--color-primary);
|
||||
--ag-alpine-active-color: var(--color-primary);
|
||||
--ag-background-color: var(--color-bg-container);
|
||||
--ag-foreground-color: var(--colorText);
|
||||
--ag-borders-critical: solid 1px;
|
||||
--ag-critical-border-color: var(--color-border-secondary);
|
||||
--ag-borders: 1px solid;
|
||||
--ag-borders-input: solid 1px;
|
||||
--ag-border-color: var(--color-border-secondary);
|
||||
--ag-secondary-border-color: var(--color-border-secondary);
|
||||
--ag-secondary-foreground-color: var(--color-text-tertiary);
|
||||
/* --ag-border-radius: 2px; */
|
||||
--ag-header-column-separator-display: block;
|
||||
--ag-header-column-separator-height: 30%;
|
||||
--ag-header-column-separator-width: 2px;
|
||||
--ag-header-column-separator-color: var(--color-fill-secondary);
|
||||
--ag-font-size: var(--fontSize);
|
||||
--ag-header-background-color: white;
|
||||
--ag-selected-row-background-color: var(--color-border-primary);
|
||||
--ag-range-selection-border-color: var(--color-border-primary);
|
||||
--ag-header-font-size: var(--fontSize);
|
||||
--ag-header-font-weight: 600;
|
||||
--ag-header-foreground-color: var(--color-primary);
|
||||
--ag-row-border-style: solid;
|
||||
--ag-row-border-width: 1px;
|
||||
--ag-row-border-color: var(--color-border-secondary);
|
||||
--ag-row-hover-color: var(--color-bg-text-hover);
|
||||
--ag-padding-horizontal: 0.7rem;
|
||||
--ag-padding-vertical: 0.9rem;
|
||||
--ag-side-panel-border-width: 1px;
|
||||
--ag-side-panel-border-color: var(--color-border-secondary);
|
||||
--ag-spacing: 6px;
|
||||
--ag-odd-row-background-color: var(--color-fill-quaternary);
|
||||
--ag-wrapper-border-width: 0px;
|
||||
/* --ag-wrapper-border-color: var(--color-border-secondary); */
|
||||
/* --ag-wrapper-border-radius: 10px; */
|
||||
}
|
||||
|
||||
.ag-root-wrapper {
|
||||
border: 0px;
|
||||
}
|
||||
border: 0px;
|
||||
}
|
||||
.assessment-standard-table .ant-table {
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.assessment-standard-table .ant-table-thead > tr > th {
|
||||
background-color: #f9fafb;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
text-align: center;
|
||||
padding: 12px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.assessment-standard-table .ant-table-tbody > tr > td {
|
||||
text-align: center;
|
||||
padding: 12px 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.assessment-standard-table .ant-table-tbody > tr:hover > td {
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
.assessment-standard-table .ant-input {
|
||||
text-align: center;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.assessment-standard-table .ant-input:focus {
|
||||
border-color: #9ca3af;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
@ -6,23 +6,51 @@ export default function AssessmentModal() {
|
|||
<div>
|
||||
<Modal
|
||||
title="添加年龄范围"
|
||||
visible={isAgeModalVisible}
|
||||
open={isAgeModalVisible}
|
||||
onOk={ageForm.submit}
|
||||
onCancel={handleAgeCancel}
|
||||
>
|
||||
<Form form={ageForm} onFinish={handleAgeOk}>
|
||||
<Form.Item name="start" label="起始年龄" rules={[{ required: true }]}>
|
||||
<Form.Item
|
||||
name="start"
|
||||
label="起始年龄"
|
||||
rules={[
|
||||
{ required: true },
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
const end = getFieldValue('end');
|
||||
if (end && value > end) {
|
||||
return Promise.reject(new Error('起始年龄不能大于结束年龄'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item name="end" label="结束年龄">
|
||||
<Form.Item
|
||||
name="end"
|
||||
label="结束年龄"
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
const start = getFieldValue('start');
|
||||
if (value && start > value) {
|
||||
return Promise.reject(new Error('结束年龄不能小于起始年龄'));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="添加分数与对应标准"
|
||||
visible={isScoreModalVisible}
|
||||
open={isScoreModalVisible}
|
||||
onOk={scoreForm.submit}
|
||||
onCancel={handleScoreCancel}
|
||||
>
|
||||
|
|
|
@ -9,6 +9,6 @@ export default function AssessmentStandardLayout() {
|
|||
<AssessmentModal></AssessmentModal>
|
||||
<SportCreateContent></SportCreateContent>
|
||||
<StandardCreateContent></StandardCreateContent>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import React, {
|
|||
// import { useDebounce } from "use-debounce";
|
||||
import { Form, FormInstance } from 'antd';
|
||||
import { api } from "@nice/client";
|
||||
import { TaxonomySlug } from "packages/common/dist";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface AssessmentStandardContextType {
|
||||
form: FormInstance;
|
||||
|
@ -58,21 +58,36 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
|
|||
const showAgeModal = () => {
|
||||
setIsAgeModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理年龄范围模态框确定
|
||||
const handleAgeOk = (values: any) => {
|
||||
console.log('values', values)
|
||||
const { start, end } = values;
|
||||
if (end && start == end) {
|
||||
toast.error("年龄范围不能相同");
|
||||
return;
|
||||
}
|
||||
//年龄校验
|
||||
const isOverlap = ageRanges.some(range => {
|
||||
if (end) {
|
||||
return (start >= range.start && start <= (range.end || Infinity)) ||
|
||||
(end >= range.start && end <= (range.end || Infinity)) ||
|
||||
(start <= range.start && end >= (range.end || Infinity));
|
||||
} else {
|
||||
return start >= range.start && start <= (range.end || Infinity);
|
||||
}
|
||||
});
|
||||
if (isOverlap) {
|
||||
toast.error("年龄范围不能与已存在的范围重叠");
|
||||
return;
|
||||
}
|
||||
const newRange = {
|
||||
start,
|
||||
end,
|
||||
label: end ? `${start}-${end}岁` : `${start}岁以上`,
|
||||
};
|
||||
setAgeRanges([...ageRanges, newRange]);
|
||||
|
||||
setIsAgeModalVisible(false);
|
||||
};
|
||||
|
||||
// 处理年龄范围模态框取消
|
||||
const handleAgeCancel = () => {
|
||||
setIsAgeModalVisible(false);
|
||||
|
@ -82,7 +97,6 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
|
|||
const showScoreModal = () => {
|
||||
setIsScoreModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理分数标准模态框确定
|
||||
const handleScoreOk = async (values: any) => {
|
||||
const { score, standards } = values;
|
||||
|
@ -98,6 +112,7 @@ export function AssessmentStandardProvider({ children }: AssessmentStandardProvi
|
|||
const handleScoreCancel = () => {
|
||||
setIsScoreModalVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<AssessmentStandardContext.Provider
|
||||
value={{
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Button, Form, Input, Select, Skeleton } from "antd";
|
||||
import { Button, Form, Input, Select, Skeleton, Pagination } from "antd";
|
||||
import { useAssessmentStandardContext } from "./assessment-standard-provider";
|
||||
import { api, useSport } from "@nice/client";
|
||||
import toast from "react-hot-toast";
|
||||
import create from "@ant-design/icons/lib/components/IconFont";
|
||||
|
||||
import { useState } from 'react';
|
||||
const { Search } = Input;
|
||||
export default function SportCreateContent() {
|
||||
const { form, sportProjectList, sportProjectLoading } = useAssessmentStandardContext();
|
||||
const { createSportProject, softDeleteByIds } = useSport();
|
||||
|
@ -13,6 +13,9 @@ export default function SportCreateContent() {
|
|||
title: "体能考核"
|
||||
}
|
||||
})
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const handleCreateProject = async () => {
|
||||
if (form.getFieldsValue().createProjectName && form.getFieldsValue().unit) {
|
||||
await createSportProject.mutateAsync({
|
||||
|
@ -37,16 +40,20 @@ export default function SportCreateContent() {
|
|||
}
|
||||
}
|
||||
const handleDeleteProject = async (id: string) => {
|
||||
console.log(id)
|
||||
// console.log(id)
|
||||
await softDeleteByIds.mutateAsync({
|
||||
ids: [id]
|
||||
} as any)
|
||||
toast.success("删除项目成功")
|
||||
}
|
||||
const handleSearch = (value) => {
|
||||
setSearchValue(value);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Form form={form} layout="vertical">
|
||||
<div className="flex items-center space-x-4 w-1/2">
|
||||
<div className="flex items-center space-x-4 w-1/3">
|
||||
<Form.Item label="创建项目" name="createProjectName">
|
||||
<Input placeholder="请输入创建的项目名称" className="mr-2" />
|
||||
</Form.Item>
|
||||
|
@ -64,16 +71,48 @@ export default function SportCreateContent() {
|
|||
</div>
|
||||
{sportProjectLoading ?
|
||||
<Skeleton></Skeleton> :
|
||||
<div className='w-1/3 my-3 max-h-48 overflow-y-auto'>
|
||||
<div className='flex my-3 max-h-48 overflow-y-auto'>
|
||||
{sportProjectList?.filter(item=>item.deletedAt === null)?.map((item) => (
|
||||
<div key={item.id} className='w-full flex justify-between p-4 mt-2 bg-white rounded-md'>
|
||||
<div className='font-bold'>{item.name}({item.unit})</div>
|
||||
<span className='text-red-500 cursor-pointer' onClick={() => handleDeleteProject(item.id)}>删除</span>
|
||||
<div
|
||||
key={item.id}
|
||||
className='w-full flex justify-between items-center p-4 my-3 bg-white rounded-lg shadow-sm border border-gray-100 hover:shadow-md transition-shadow duration-200'
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<span className='h-4 w-1 bg-blue-500 rounded-full mr-3'></span>
|
||||
<div className='font-medium text-gray-800'>
|
||||
{item.name}
|
||||
<span className='text-gray-500 text-sm ml-2'>({item.unit})</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className='text-gray-400 hover:text-red-500 transition-colors duration-200 flex items-center gap-1 px-2 py-1 rounded-md hover:bg-red-50'
|
||||
onClick={() => handleDeleteProject(item.id)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
<span>删除</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
</Form>
|
||||
{/* <Search
|
||||
placeholder="搜索..."
|
||||
allowClear
|
||||
value={searchValue}
|
||||
onChange={(e) => {
|
||||
if (!e.target.value) {
|
||||
setSearchValue('');
|
||||
setCurrentPage(1);
|
||||
} else {
|
||||
setSearchValue(e.target.value);
|
||||
}
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
style={{ width: 200 }}
|
||||
/> */}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -3,6 +3,7 @@ import { useAssessmentStandardContext } from "./assessment-standard-provider";
|
|||
import toast from "react-hot-toast";
|
||||
import { api, useSport } from "@nice/client";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import '../../../App.css'
|
||||
export default function StandardCreateContent() {
|
||||
const {form,sportProjectList,ageRanges,records,showAgeModal,showScoreModal,setRecords,setAgeRanges,isStandardCreate,setIsStandardCreate} = useAssessmentStandardContext();
|
||||
const { createSportStandard,updateSportStandard } = useSport();
|
||||
|
@ -28,6 +29,9 @@ export default function StandardCreateContent() {
|
|||
dataIndex: 'score',
|
||||
key: 'score',
|
||||
width: 100,
|
||||
render: (score: number) => (
|
||||
<span className="font-medium text-gray-700">{score}</span>
|
||||
)
|
||||
},
|
||||
...ageRanges.map((range, index) => ({
|
||||
title: range.label,
|
||||
|
@ -35,7 +39,6 @@ export default function StandardCreateContent() {
|
|||
key: `standard[${index}]`,
|
||||
render: (_: any, record: any) => (
|
||||
<Input
|
||||
style={{ width: '80px' }}
|
||||
value={record.standards[index]}
|
||||
onChange={(e) => {
|
||||
const inputValue = e.target.value;
|
||||
|
@ -67,6 +70,25 @@ export default function StandardCreateContent() {
|
|||
}
|
||||
}
|
||||
const createStandard = async ()=>{
|
||||
// 新增年龄范围校验
|
||||
const sortedRanges = [...ageRanges].sort((a, b) => a.start - b.start);
|
||||
for (let i = 0; i < sortedRanges.length - 1; i++) {
|
||||
const current = sortedRanges[i];
|
||||
const next = sortedRanges[i + 1];
|
||||
|
||||
// 检查是否有重叠
|
||||
if ((current.end || Infinity) >= next.start) {
|
||||
toast.error(`年龄范围 ${current.label} 和 ${next.label} 重叠`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否有间隔
|
||||
if ((current.end || Infinity) + 1 !== next.start) {
|
||||
toast.error(`年龄范围 ${current.label} 和 ${next.label} 不连续`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await createSportStandard.mutateAsync({
|
||||
data: {
|
||||
projectId: form.getFieldsValue().projectId,
|
||||
|
@ -80,6 +102,23 @@ export default function StandardCreateContent() {
|
|||
toast.success("保存标准成功")
|
||||
}
|
||||
const updateStandard = async ()=>{
|
||||
// 新增年龄范围校验
|
||||
const sortedRanges = [...ageRanges].sort((a, b) => a.start - b.start);
|
||||
for (let i = 0; i < sortedRanges.length - 1; i++) {
|
||||
const current = sortedRanges[i];
|
||||
const next = sortedRanges[i + 1];
|
||||
|
||||
if ((current.end || Infinity) >= next.start) {
|
||||
toast.error(`年龄范围 ${current.label} 和 ${next.label} 重叠`);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((current.end || Infinity) + 1 !== next.start) {
|
||||
toast.error(`年龄范围 ${current.label} 和 ${next.label} 不连续`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await updateSportStandard.mutateAsync({
|
||||
data: {
|
||||
id: data[0].id,
|
||||
|
@ -101,31 +140,36 @@ export default function StandardCreateContent() {
|
|||
});
|
||||
useEffect(() => {
|
||||
if (data && data.length) {
|
||||
setIsStandardCreate(false)
|
||||
const records: {
|
||||
score: number;
|
||||
standards: number[];
|
||||
}[] = Object.entries(JSON.parse(String(data[0].scoreTable))).map(([score, standards]) => ({
|
||||
setIsStandardCreate(false);
|
||||
const records = Object.entries(JSON.parse(String(data[0].scoreTable))).map(([score, standards]) => ({
|
||||
score: Number(score),
|
||||
standards: standards as number[]
|
||||
}));
|
||||
setAgeRanges(JSON.parse(String(data[0].ageRanges)))
|
||||
setRecords(records)
|
||||
setAgeRanges(JSON.parse(String(data[0].ageRanges)));
|
||||
setRecords(records);
|
||||
} else {
|
||||
// 如果没有找到数据,重置状态
|
||||
setIsStandardCreate(true);
|
||||
setAgeRanges([]);
|
||||
setRecords([]);
|
||||
}
|
||||
}, [data])
|
||||
|
||||
}, [data, projectId, gender, personType]); // 添加更多依赖项
|
||||
return (
|
||||
<Form form={form} layout="vertical">
|
||||
|
||||
<Space size="large" className="my-6">
|
||||
<Form.Item label="项目" name="projectId">
|
||||
<Select
|
||||
style={{ width: 200 }}
|
||||
placeholder="选择考核项目"
|
||||
options={sportProjectList?.filter(item=>item.deletedAt === null)?.map((item) => ({ value: item.id, label: `${item.name}(${item.unit})` })) || []}
|
||||
onChange={() => {
|
||||
// 重置状态,强制重新加载
|
||||
setIsStandardCreate(true);
|
||||
setAgeRanges([]);
|
||||
setRecords([]);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="性别" name="gender">
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
|
@ -136,7 +180,6 @@ export default function StandardCreateContent() {
|
|||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="人员类型" name="personType">
|
||||
<Select
|
||||
style={{ width: 160 }}
|
||||
|
@ -151,14 +194,19 @@ export default function StandardCreateContent() {
|
|||
<Button onClick={showScoreModal} className="mt-9 ml-2">添加分数与对应标准</Button>
|
||||
<Button type="primary" onClick={handleSave} className='mt-9 ml-2'>保存标准</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={records}
|
||||
bordered
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={records}
|
||||
pagination={false}
|
||||
bordered
|
||||
rowKey="score"
|
||||
className="assessment-standard-table"
|
||||
size="middle"
|
||||
scroll={{ x: 'max-content' }}
|
||||
rowClassName={() => "bg-white"}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default function CommonAssessmentPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>CommonAssessmentPage</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue