feat: add Student-User association and activate all notification TODOs

This commit is contained in:
2026-07-05 23:42:22 +08:00
parent 9e6c136ef0
commit 9d9d4719b5
6 changed files with 116 additions and 10 deletions

View File

@@ -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<Student>,
@InjectRepository(Bill) private billRepo: Repository<Bill>,
) {}
@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;
}

View File

@@ -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,
],

View File

@@ -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<Student>,
) {}
@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;
}

View File

@@ -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],

View File

@@ -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;

View File

@@ -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<Student>,
) {}
@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;
}