From 9d9d4719b53d1b3cf7dcbb04a028abc1c27e0ea0 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 5 Jul 2026 23:42:22 +0800 Subject: [PATCH] feat: add Student-User association and activate all notification TODOs --- apps/server/src/bills/bills.controller.ts | 52 +++++++++++++++++-- apps/server/src/bills/bills.module.ts | 2 + .../src/deposits/deposits.controller.ts | 30 ++++++++++- apps/server/src/deposits/deposits.module.ts | 3 +- apps/server/src/entities/student.entity.ts | 9 ++++ .../src/occupancies/occupancies.controller.ts | 30 ++++++++++- 6 files changed, 116 insertions(+), 10 deletions(-) diff --git a/apps/server/src/bills/bills.controller.ts b/apps/server/src/bills/bills.controller.ts index faaaf64..76f9828 100644 --- a/apps/server/src/bills/bills.controller.ts +++ b/apps/server/src/bills/bills.controller.ts @@ -12,9 +12,13 @@ import { Res, Req, } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, In } from 'typeorm'; import { BillsService } from './bills.service'; import { NotificationsService } from '../notifications/notifications.service'; import { NotificationType } from '../entities/notification.entity'; +import { Student } from '../entities/student.entity'; +import { Bill } from '../entities/bill.entity'; import { BillsExportService } from './bills-export.service'; import { GenerateBillsDto, UpdateBillStatusDto } from './dto/bill.dto'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; @@ -23,14 +27,14 @@ import { extractRequestInfo } from '../common/request-utils'; import { RequirePermission } from '../auth/decorators/permission.decorator'; import type { Response } from 'express'; -@UseGuards(JwtAuthGuard) -@Controller('bills') export class BillsController { constructor( private service: BillsService, private exportService: BillsExportService, private logService: OperationLogsService, private readonly notificationsService: NotificationsService, + @InjectRepository(Student) private studentRepo: Repository, + @InjectRepository(Bill) private billRepo: Repository, ) {} @Post('generate') @@ -47,7 +51,20 @@ export class BillsController { ipAddress, userAgent, }); - // TODO: Send notifications for bill_generated — studentId→userId mapping unavailable + // Send bill_generated notifications + try { + for (const bill of result.bills) { + const student = await this.studentRepo.findOne({ where: { id: bill.studentId } }); + if (student?.userId) { + void this.notificationsService.create({ + recipientIds: [student.userId], + type: NotificationType.BILL_GENERATED, + title: '新账单', + content: `您有一笔新账单,金额: ¥${bill.totalAmount}, 周期: ${dto.periodStart}~${dto.periodEnd}`, + }); + } + } + } catch (_) { /* don't block response */ } return result; } @@ -92,7 +109,18 @@ export class BillsController { ipAddress, userAgent, }); - // TODO: Send notification for bill_paid — bill.studentId→userId mapping unavailable + // Send bill_paid notification + try { + const student = await this.studentRepo.findOne({ where: { id: result.studentId } }); + if (student?.userId) { + void this.notificationsService.create({ + recipientIds: [student.userId], + type: NotificationType.BILL_PAID, + title: '账单已确认', + content: `账单 #${result.id} 已确认收款,金额: ¥${result.totalAmount}`, + }); + } + } catch (_) { /* don't block response */ } return result; } @@ -110,7 +138,21 @@ export class BillsController { ipAddress, userAgent, }); - // TODO: Send notification for bill_paid (batch) — bill.studentId→userId mapping unavailable + // Send bill_paid notifications (batch) + try { + const bills = await this.billRepo.findBy({ id: In(body.ids) }); + for (const bill of bills) { + const student = await this.studentRepo.findOne({ where: { id: bill.studentId } }); + if (student?.userId) { + void this.notificationsService.create({ + recipientIds: [student.userId], + type: NotificationType.BILL_PAID, + title: '账单已确认', + content: `账单 #${bill.id} 已确认收款,金额: ¥${bill.totalAmount}`, + }); + } + } + } catch (_) { /* don't block response */ } return result; } diff --git a/apps/server/src/bills/bills.module.ts b/apps/server/src/bills/bills.module.ts index 2da2e6b..14dfd6c 100644 --- a/apps/server/src/bills/bills.module.ts +++ b/apps/server/src/bills/bills.module.ts @@ -8,6 +8,7 @@ import { PersonalExpense } from '../entities/personal-expense.entity'; import { Occupancy } from '../entities/occupancy.entity'; import { Room } from '../entities/room.entity'; import { Deposit } from '../entities/deposit.entity'; +import { Student } from '../entities/student.entity'; import { BillsService } from './bills.service'; import { BillsExportService } from './bills-export.service'; import { BillsController } from './bills.controller'; @@ -22,6 +23,7 @@ import { BillsController } from './bills.controller'; Occupancy, Room, Deposit, + Student, ]), NotificationsModule, ], diff --git a/apps/server/src/deposits/deposits.controller.ts b/apps/server/src/deposits/deposits.controller.ts index 374b654..636ff4d 100644 --- a/apps/server/src/deposits/deposits.controller.ts +++ b/apps/server/src/deposits/deposits.controller.ts @@ -10,6 +10,9 @@ import { UseGuards, Request, } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Student } from '../entities/student.entity'; import { DepositsService } from './deposits.service'; import { NotificationsService } from '../notifications/notifications.service'; import { NotificationType } from '../entities/notification.entity'; @@ -26,6 +29,7 @@ export class DepositsController { private service: DepositsService, private logService: OperationLogsService, private readonly notificationsService: NotificationsService, + @InjectRepository(Student) private studentRepo: Repository, ) {} @Get() @@ -71,7 +75,18 @@ export class DepositsController { ipAddress, userAgent, }); - // TODO: Send notification for deposit_due — studentId→userId mapping unavailable + // Send deposit_due notification + try { + const student = await this.studentRepo.findOne({ where: { id: dto.studentId } }); + if (student?.userId) { + void this.notificationsService.create({ + recipientIds: [student.userId], + type: 'deposit_due', + title: '押金待缴', + content: `您有一笔押金待缴纳,金额: ¥${dto.amount}`, + }); + } + } catch (_) { /* don't block response */ } return result; } @@ -115,7 +130,18 @@ export class DepositsController { ipAddress, userAgent, }); - // TODO: Send notification for deposit_refunded — studentId→userId mapping unavailable + // Send deposit_refunded notification + try { + const student = await this.studentRepo.findOne({ where: { id: result.studentId } }); + if (student?.userId) { + void this.notificationsService.create({ + recipientIds: [student.userId], + type: 'deposit_refunded', + title: '押金已退还', + content: `您的押金已退还,退还¥${result.refundAmount},扣除¥${result.deductionAmount}`, + }); + } + } catch (_) { /* don't block response */ } return result; } diff --git a/apps/server/src/deposits/deposits.module.ts b/apps/server/src/deposits/deposits.module.ts index 545a9d7..5c49211 100644 --- a/apps/server/src/deposits/deposits.module.ts +++ b/apps/server/src/deposits/deposits.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { Student } from '../entities/student.entity'; import { Deposit } from '../entities/deposit.entity'; import { DepositInstallment } from '../entities/deposit-installment.entity'; import { DepositsService } from './deposits.service'; @@ -8,7 +9,7 @@ import { OperationLogsModule } from '../operation-logs/operation-logs.module'; import { NotificationsModule } from '../notifications/notifications.module'; @Module({ - imports: [TypeOrmModule.forFeature([Deposit, DepositInstallment]), OperationLogsModule, NotificationsModule], + imports: [TypeOrmModule.forFeature([Deposit, DepositInstallment, Student]), OperationLogsModule, NotificationsModule], controllers: [DepositsController], providers: [DepositsService], exports: [DepositsService], diff --git a/apps/server/src/entities/student.entity.ts b/apps/server/src/entities/student.entity.ts index 7f98acd..1441711 100644 --- a/apps/server/src/entities/student.entity.ts +++ b/apps/server/src/entities/student.entity.ts @@ -5,6 +5,7 @@ import { CreateDateColumn, UpdateDateColumn, ManyToOne, + OneToOne, JoinColumn, OneToMany, } from 'typeorm'; @@ -12,6 +13,7 @@ import { Occupancy } from './occupancy.entity'; import { PersonalExpense } from './personal-expense.entity'; import { Bill } from './bill.entity'; import { Tenant } from './tenant.entity'; +import { User } from './user.entity'; @Entity('students') export class Student { @@ -59,6 +61,13 @@ export class Student { @OneToMany(() => Occupancy, (o) => o.student) occupancies: Occupancy[]; + @Column({ name: 'user_id', type: 'integer', nullable: true, unique: true }) + userId: number; + + @OneToOne(() => User, { nullable: true }) + @JoinColumn({ name: 'user_id' }) + user: User; + @Column({ name: 'tenant_id', type: 'integer', nullable: true }) tenantId: number; diff --git a/apps/server/src/occupancies/occupancies.controller.ts b/apps/server/src/occupancies/occupancies.controller.ts index 9a2bace..3894ae3 100644 --- a/apps/server/src/occupancies/occupancies.controller.ts +++ b/apps/server/src/occupancies/occupancies.controller.ts @@ -13,6 +13,9 @@ import { UseInterceptors, UploadedFile, } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Student } from '../entities/student.entity'; import { FileInterceptor } from '@nestjs/platform-express'; import type { Response } from 'express'; import { OccupanciesService } from './occupancies.service'; @@ -32,6 +35,7 @@ export class OccupanciesController { private service: OccupanciesService, private logService: OperationLogsService, private readonly notificationsService: NotificationsService, + @InjectRepository(Student) private studentRepo: Repository, ) {} @Get() @@ -81,7 +85,18 @@ export class OccupanciesController { ipAddress, userAgent, }); - // TODO: Send notification for check_in — studentId→userId mapping unavailable + // Send check_in notification + try { + const student = await this.studentRepo.findOne({ where: { id: dto.studentId } }); + if (student?.userId) { + void this.notificationsService.create({ + recipientIds: [student.userId], + type: NotificationType.CHECK_IN, + title: '入住通知', + content: `您已入住房间 #${dto.roomId}`, + }); + } + } catch (_) { /* don't block response */ } return result; } @@ -100,7 +115,18 @@ export class OccupanciesController { ipAddress, userAgent, }); - // TODO: Send notification for check_out — studentId→userId mapping unavailable + // Send check_out notification + try { + const student = await this.studentRepo.findOne({ where: { id: result.studentId } }); + if (student?.userId) { + void this.notificationsService.create({ + recipientIds: [student.userId], + type: NotificationType.CHECK_OUT, + title: '退宿通知', + content: `您已退宿房间 #${result.roomId}`, + }); + } + } catch (_) { /* don't block response */ } return result; }