diff --git a/apps/server/src/bills/bills.controller.ts b/apps/server/src/bills/bills.controller.ts index fe69d24..d057a65 100644 --- a/apps/server/src/bills/bills.controller.ts +++ b/apps/server/src/bills/bills.controller.ts @@ -7,6 +7,7 @@ import { Param, Body, Query, + ParseIntPipe, UseGuards, Request, Res, @@ -20,7 +21,11 @@ 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 { + BatchUpdateBillStatusDto, + 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'; @@ -88,47 +93,15 @@ export class BillsController { @Get(':id') @RequirePermission('bill:view') - findOne(@Param('id') id: string) { - return this.service.findOne(+id); - } - - @Put(':id/status') - @RequirePermission('bill:confirm') - async updateStatus( - @Param('id') id: string, - @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; + findOne(@Param('id', ParseIntPipe) id: number) { + return this.service.findOne(id); } + // Static routes must be declared before /:id/status, otherwise "batch" is + // treated as an id and converted to NaN by the parameterized route. @Put('batch/status') @RequirePermission('bill:confirm') - async batchUpdateStatus(@Body() body: { ids: number[]; status: string }, @Request() req: any) { + async batchUpdateStatus(@Body() body: BatchUpdateBillStatusDto, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); const result = await this.service.batchUpdateStatus(body.ids, body.status); await this.logService.log({ @@ -158,17 +131,51 @@ export class BillsController { return result; } + @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; + } + @Delete(':id') @RequirePermission('bill:delete') - async remove(@Param('id') id: string, @Request() req: any) { + async remove(@Param('id', ParseIntPipe) id: number, @Request() req: any) { const { ipAddress, userAgent } = extractRequestInfo(req); - const result = await this.service.remove(+id); + const result = await this.service.remove(id); await this.logService.log({ userId: req.user?.id, username: req.user?.username, module: '账单管理', action: '删除账单', - targetId: +id, + targetId: id, targetType: 'bill', ipAddress, userAgent, @@ -226,18 +233,18 @@ export class BillsController { @Get('export/pdf/:id') @RequirePermission('bill:export-pdf') - async exportPdf(@Param('id') id: string, @Res() res: Response, @Req() req: any) { + 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, + targetId: id, targetType: 'bill', ipAddress, userAgent, }); - return this.exportService.exportStudentPdf(+id, res); + return this.exportService.exportStudentPdf(id, res); } } diff --git a/apps/server/src/bills/dto/bill.dto.ts b/apps/server/src/bills/dto/bill.dto.ts index b25f783..20016b5 100644 --- a/apps/server/src/bills/dto/bill.dto.ts +++ b/apps/server/src/bills/dto/bill.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsOptional } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsIn, IsInt, IsString } from 'class-validator'; export class GenerateBillsDto { @IsString() @@ -9,6 +9,13 @@ export class GenerateBillsDto { } export class UpdateBillStatusDto { - @IsString() + @IsIn(['draft', 'confirmed', 'paid']) status: 'draft' | 'confirmed' | 'paid'; } + +export class BatchUpdateBillStatusDto extends UpdateBillStatusDto { + @IsArray() + @ArrayNotEmpty() + @IsInt({ each: true }) + ids: number[]; +}