diff --git a/apps/server/src/bills/bills.controller.ts b/apps/server/src/bills/bills.controller.ts index e3c04f0..faaaf64 100644 --- a/apps/server/src/bills/bills.controller.ts +++ b/apps/server/src/bills/bills.controller.ts @@ -13,6 +13,8 @@ import { Req, } from '@nestjs/common'; import { BillsService } from './bills.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationType } from '../entities/notification.entity'; import { BillsExportService } from './bills-export.service'; import { GenerateBillsDto, UpdateBillStatusDto } from './dto/bill.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @@ -28,6 +30,7 @@ export class BillsController { private service: BillsService, private exportService: BillsExportService, private logService: OperationLogsService, + private readonly notificationsService: NotificationsService, ) {} @Post('generate') @@ -44,6 +47,7 @@ export class BillsController { ipAddress, userAgent, }); + // TODO: Send notifications for bill_generated — studentId→userId mapping unavailable return result; } @@ -88,6 +92,7 @@ export class BillsController { ipAddress, userAgent, }); + // TODO: Send notification for bill_paid — bill.studentId→userId mapping unavailable return result; } @@ -105,6 +110,7 @@ export class BillsController { ipAddress, userAgent, }); + // TODO: Send notification for bill_paid (batch) — bill.studentId→userId mapping unavailable return result; } diff --git a/apps/server/src/bills/bills.module.ts b/apps/server/src/bills/bills.module.ts index 1ec7595..2da2e6b 100644 --- a/apps/server/src/bills/bills.module.ts +++ b/apps/server/src/bills/bills.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { NotificationsModule } from '../notifications/notifications.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Bill } from '../entities/bill.entity'; import { BillItem } from '../entities/bill-item.entity'; @@ -22,6 +23,7 @@ import { BillsController } from './bills.controller'; Room, Deposit, ]), + NotificationsModule, ], controllers: [BillsController], providers: [BillsService, BillsExportService], diff --git a/apps/server/src/classes/classes.controller.ts b/apps/server/src/classes/classes.controller.ts index 3a647c4..4bb81a2 100644 --- a/apps/server/src/classes/classes.controller.ts +++ b/apps/server/src/classes/classes.controller.ts @@ -24,6 +24,8 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { extractRequestInfo } from '../common/request-utils'; import { RequirePermission } from '../auth/decorators/permission.decorator'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationType } from '../entities/notification.entity'; import * as ExcelJS from 'exceljs'; @UseGuards(JwtAuthGuard) @@ -32,6 +34,7 @@ export class ClassesController { constructor( private readonly service: ClassesService, private readonly logService: OperationLogsService, + private readonly notificationsService: NotificationsService, ) {} @Get() @@ -170,6 +173,17 @@ export class ClassesController { ipAddress, userAgent, }); + try { + const cls = await this.service.findOne(+id); + if (cls.headTeacherId) { + void this.notificationsService.create({ + recipientIds: [cls.headTeacherId], + type: NotificationType.CLASS_CHANGE, + title: '学员变动', + content: `班级新增${result.added}名学生`, + }); + } + } catch {} return result; } @@ -222,6 +236,14 @@ export class ClassesController { ipAddress, userAgent, }); + try { + void this.notificationsService.create({ + recipientIds: [dto.userId], + type: NotificationType.CLASS_CHANGE, + title: '班级分配', + content: `您已被分配到班级担任${dto.roleType}角色`, + }); + } catch {} return result; } diff --git a/apps/server/src/classes/classes.module.ts b/apps/server/src/classes/classes.module.ts index b95f6dc..a99c715 100644 --- a/apps/server/src/classes/classes.module.ts +++ b/apps/server/src/classes/classes.module.ts @@ -4,9 +4,10 @@ import { Class, ClassStudent, ClassTeacher } from '../entities'; import { ClassesService } from './classes.service'; import { ClassesController } from './classes.controller'; import { OperationLogsModule } from '../operation-logs/operation-logs.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher]), OperationLogsModule], + imports: [TypeOrmModule.forFeature([Class, ClassStudent, ClassTeacher]), OperationLogsModule, NotificationsModule], controllers: [ClassesController], providers: [ClassesService], exports: [ClassesService], diff --git a/apps/server/src/deposits/deposits.controller.ts b/apps/server/src/deposits/deposits.controller.ts index 3e086da..374b654 100644 --- a/apps/server/src/deposits/deposits.controller.ts +++ b/apps/server/src/deposits/deposits.controller.ts @@ -11,6 +11,8 @@ import { Request, } from '@nestjs/common'; import { DepositsService } from './deposits.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationType } from '../entities/notification.entity'; import { CreateDepositDto, RefundDepositDto, CreateDepositWithInstallmentsDto } from './dto/deposit.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; @@ -23,6 +25,7 @@ export class DepositsController { constructor( private service: DepositsService, private logService: OperationLogsService, + private readonly notificationsService: NotificationsService, ) {} @Get() @@ -68,6 +71,7 @@ export class DepositsController { ipAddress, userAgent, }); + // TODO: Send notification for deposit_due — studentId→userId mapping unavailable return result; } @@ -111,6 +115,7 @@ export class DepositsController { ipAddress, userAgent, }); + // TODO: Send notification for deposit_refunded — studentId→userId mapping unavailable return result; } diff --git a/apps/server/src/deposits/deposits.module.ts b/apps/server/src/deposits/deposits.module.ts index bcb16b6..545a9d7 100644 --- a/apps/server/src/deposits/deposits.module.ts +++ b/apps/server/src/deposits/deposits.module.ts @@ -5,9 +5,10 @@ import { DepositInstallment } from '../entities/deposit-installment.entity'; import { DepositsService } from './deposits.service'; import { DepositsController } from './deposits.controller'; import { OperationLogsModule } from '../operation-logs/operation-logs.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([Deposit, DepositInstallment]), OperationLogsModule], + imports: [TypeOrmModule.forFeature([Deposit, DepositInstallment]), OperationLogsModule, NotificationsModule], controllers: [DepositsController], providers: [DepositsService], exports: [DepositsService], diff --git a/apps/server/src/occupancies/occupancies.controller.ts b/apps/server/src/occupancies/occupancies.controller.ts index 13cb460..9a2bace 100644 --- a/apps/server/src/occupancies/occupancies.controller.ts +++ b/apps/server/src/occupancies/occupancies.controller.ts @@ -16,6 +16,8 @@ import { import { FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import { OccupanciesService } from './occupancies.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationType } from '../entities/notification.entity'; import { CheckInDto, CheckOutDto, TransferRoomDto, BatchCheckOutDto } from './dto/occupancy.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; @@ -29,6 +31,7 @@ export class OccupanciesController { constructor( private service: OccupanciesService, private logService: OperationLogsService, + private readonly notificationsService: NotificationsService, ) {} @Get() @@ -78,6 +81,7 @@ export class OccupanciesController { ipAddress, userAgent, }); + // TODO: Send notification for check_in — studentId→userId mapping unavailable return result; } @@ -96,6 +100,7 @@ export class OccupanciesController { ipAddress, userAgent, }); + // TODO: Send notification for check_out — studentId→userId mapping unavailable return result; } diff --git a/apps/server/src/occupancies/occupancies.module.ts b/apps/server/src/occupancies/occupancies.module.ts index 6cc4320..e583969 100644 --- a/apps/server/src/occupancies/occupancies.module.ts +++ b/apps/server/src/occupancies/occupancies.module.ts @@ -7,9 +7,10 @@ import { Deposit } from '../entities/deposit.entity'; import { OccupanciesService } from './occupancies.service'; import { OccupanciesController } from './occupancies.controller'; import { OperationLogsModule } from '../operation-logs/operation-logs.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([Occupancy, Room, Student, Deposit]), OperationLogsModule], + imports: [TypeOrmModule.forFeature([Occupancy, Room, Student, Deposit]), OperationLogsModule, NotificationsModule], controllers: [OccupanciesController], providers: [OccupanciesService], exports: [OccupanciesService], diff --git a/apps/server/src/schedules/schedules.controller.ts b/apps/server/src/schedules/schedules.controller.ts index 6acedfd..3922305 100644 --- a/apps/server/src/schedules/schedules.controller.ts +++ b/apps/server/src/schedules/schedules.controller.ts @@ -20,6 +20,9 @@ import { import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { OperationLogsService } from '../operation-logs/operation-logs.service'; import { extractRequestInfo } from '../common/request-utils'; +import { ConflictException } from '@nestjs/common'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationType } from '../entities/notification.entity'; import { RequirePermission } from '../auth/decorators/permission.decorator'; @UseGuards(JwtAuthGuard) @@ -28,6 +31,7 @@ export class SchedulesController { constructor( private readonly service: SchedulesService, private readonly logService: OperationLogsService, + private readonly notificationsService: NotificationsService, ) {} @Get() @@ -61,19 +65,39 @@ export class SchedulesController { @RequirePermission('schedule:create') async create(@Body() dto: CreateScheduleDto, @Request() req: { user?: { id: number; username: string }; headers?: Record }) { const { ipAddress, userAgent } = extractRequestInfo(req); - const result = await this.service.create(dto); - await this.logService.log({ - userId: req.user?.id, - username: req.user?.username, - module: '排课管理', - action: '创建排课', - targetId: result.id, - targetType: 'class-schedule', - detail: `${result.subject} 周${result.weekDay} ${result.startTime}-${result.endTime}`, - ipAddress, - userAgent, - }); - return result; + try { + const result = await this.service.create(dto); + await this.logService.log({ + userId: req.user?.id, + username: req.user?.username, + module: '排课管理', + action: '创建排课', + targetId: result.id, + targetType: 'class-schedule', + detail: `${result.subject} 周${result.weekDay} ${result.startTime}-${result.endTime}`, + ipAddress, + userAgent, + }); + return result; + } catch (error) { + if (error instanceof ConflictException) { + try { + const conflicts = await this.service.checkConflict( + dto.classroomId, dto.weekDay, dto.startTime, dto.endTime, dto.startDate, dto.endDate, + ); + const teacherIds = [...new Set(conflicts.map(c => c.teacherId).filter(Boolean))]; + if (teacherIds.length > 0) { + void this.notificationsService.create({ + recipientIds: teacherIds, + type: NotificationType.SCHEDULE_CONFLICT, + title: '排课冲突', + content: `教室${dto.classroomId} 周${dto.weekDay} ${dto.startTime}-${dto.endTime} 与已有排课冲突`, + }); + } + } catch {} + } + throw error; + } } @Put(':id') @@ -84,19 +108,39 @@ export class SchedulesController { @Request() req: { user?: { id: number; username: string }; headers?: Record }, ) { const { ipAddress, userAgent } = extractRequestInfo(req); - const result = await this.service.update(+id, dto); - await this.logService.log({ - userId: req.user?.id, - username: req.user?.username, - module: '排课管理', - action: '编辑排课', - targetId: +id, - targetType: 'class-schedule', - detail: JSON.stringify(dto), - ipAddress, - userAgent, - }); - return result; + try { + const result = await this.service.update(+id, dto); + await this.logService.log({ + userId: req.user?.id, + username: req.user?.username, + module: '排课管理', + action: '编辑排课', + targetId: +id, + targetType: 'class-schedule', + detail: JSON.stringify(dto), + ipAddress, + userAgent, + }); + return result; + } catch (error) { + if (error instanceof ConflictException) { + try { + const conflicts = await this.service.checkConflict( + dto.classroomId ?? 0, dto.weekDay ?? 0, dto.startTime ?? '', dto.endTime ?? '', dto.startDate ?? '', dto.endDate ?? '', + ); + const teacherIds = [...new Set(conflicts.map(c => c.teacherId).filter(Boolean))]; + if (teacherIds.length > 0) { + void this.notificationsService.create({ + recipientIds: teacherIds, + type: NotificationType.SCHEDULE_CONFLICT, + title: '排课冲突', + content: `教室${dto.classroomId} 周${dto.weekDay} ${dto.startTime}-${dto.endTime} (更新) 与已有排课冲突`, + }); + } + } catch {} + } + throw error; + } } @Delete(':id') diff --git a/apps/server/src/schedules/schedules.module.ts b/apps/server/src/schedules/schedules.module.ts index c9a7b03..fa6d96d 100644 --- a/apps/server/src/schedules/schedules.module.ts +++ b/apps/server/src/schedules/schedules.module.ts @@ -4,9 +4,10 @@ import { ClassSchedule } from '../entities'; import { SchedulesService } from './schedules.service'; import { SchedulesController } from './schedules.controller'; import { OperationLogsModule } from '../operation-logs/operation-logs.module'; +import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([ClassSchedule]), OperationLogsModule], + imports: [TypeOrmModule.forFeature([ClassSchedule]), OperationLogsModule, NotificationsModule], controllers: [SchedulesController], providers: [SchedulesService], exports: [SchedulesService],