import { Controller, Get, Post, Put, Delete, Param, Body, Query, UseGuards, Request, Res, Req, ParseIntPipe, } 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 { CancelBillDto, GenerateBillsDto, UpdateBillStatusDto } from './dto/bill.dto'; 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 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') @RequirePermission('bill:generate') async generateBills(@Body() dto: GenerateBillsDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.generateBills(dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单管理', action: '生成账单', detail: `周期 ${result.periodStart}~${result.periodEnd}, 生成 ${result.count} 条`, ipAddress, userAgent, }); // 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}, 周期: ${result.periodStart}~${result.periodEnd}`, }); } } } catch (_) { /* don't block response */ } return result; } @Get() @RequirePermission('bill:view') findAll( @Query('periodStart') periodStart?: string, @Query('periodEnd') periodEnd?: string, @Query('studentId', new ParseIntPipe({ optional: true })) studentId?: number, @Query('status') status?: string, @Query('expenseType') expenseType?: string, ) { return this.service.findAll({ periodStart, periodEnd, studentId, status, expenseType, }); } @Get(':id') @RequirePermission('bill:view') findOne(@Param('id', ParseIntPipe) id: number) { return this.service.findOne(id); } @Put(':id/status') @RequirePermission('bill:confirm') async updateStatus( @Param('id', ParseIntPipe) id: number, @Body() dto: UpdateBillStatusDto, @Request() req: any, ) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.updateStatus(id, dto); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单管理', action: '确认账单', targetId: id, targetType: 'bill', ipAddress, userAgent, }); // 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; } @Put('batch/status') @RequirePermission('bill:confirm') async batchUpdateStatus(@Body() body: { ids: number[]; status: string }, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.batchUpdateStatus(body.ids, body.status); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单管理', action: '确认账单', detail: `IDs: ${body.ids.join(',')}`, ipAddress, userAgent, }); // 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; } @Post(':id/cancel') @RequirePermission('bill:delete') async cancel(@Param('id', ParseIntPipe) id: number, @Body() dto: CancelBillDto, @Request() req: any) { const result = await this.service.cancel(id, dto, req.user?.id); const { ipAddress, userAgent } = extractRequestInfo(req); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单管理', action: '取消账单并冲正', targetId: id, targetType: 'bill', detail: dto.reason, ipAddress, userAgent, }); return result; } @Delete(':id') @RequirePermission('bill:delete') async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.remove(id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单管理', action: '归档账单', targetId: id, targetType: 'bill', ipAddress, userAgent, }); return result; } @Post('batch/delete') @RequirePermission('bill:delete') async batchRemove(@Body() body: { ids: number[] }, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.batchRemove(body.ids); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单管理', action: '批量归档账单', detail: `IDs: ${body.ids.join(',')}`, ipAddress, userAgent, }); return result; } @Get('export/excel') @RequirePermission('bill:export-excel') async exportExcel( @Query('periodStart') periodStart?: string, @Query('periodEnd') periodEnd?: string, @Query('studentId', new ParseIntPipe({ optional: true })) studentId?: number, @Query('status') status?: string, @Res() res?: Response, @Req() req?: any, ) { const { ipAddress, userAgent } = extractRequestInfo(req); await this.logService.log({ userId: req?.user?.id, username: req?.user?.username, module: '账单管理', action: '导出账单', detail: `筛选: 周期${periodStart || '全部'}~${periodEnd || '全部'}, 状态${status || '全部'}`, ipAddress, userAgent, }); return this.exportService.exportExcel( { periodStart, periodEnd, studentId, status, }, res!, ); } @Get('export/pdf/:id') @RequirePermission('bill:export-pdf') async exportPdf(@Param('id', ParseIntPipe) id: number, @Res() res: Response, @Req() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); await this.logService.log({ userId: req?.user?.id, username: req?.user?.username, module: '账单管理', action: '导出账单', targetId: id, targetType: 'bill', ipAddress, userAgent, }); return this.exportService.exportStudentPdf(id, res); } }