import { BadRequestException, Body, Controller, Get, Param, Post, Put, Query, Res, UploadedFile, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { ApiTags } from '@nestjs/swagger'; import { RequireRecentAuthentication } from '../auth/require-recent-authentication.decorator'; import { CurrentSessionUserId } from '../auth/current-session-user.decorator'; import { ReportMaterialsService } from './report-materials.service'; import { ReportBatchDownloadService } from './batch-download.service'; import { CreateImportProfileDto, CreateReportBatchDto, ImportCommitDto, ReviewImportItemsDto, SingleReportMaterialDto, } from './report-materials.contracts'; type UploadedWorkbook = { originalname: string; mimetype: string; size: number; buffer: Buffer }; type DownloadResponse = { setHeader(name: string, value: string): void; send(content: Buffer): void }; @ApiTags('report-materials') @Controller('admin/report-materials') export class ReportMaterialsController { constructor( private readonly service: ReportMaterialsService, private readonly batchDownloads: ReportBatchDownloadService, ) {} @Get('pending') listPending( @Query('reportType') reportType?: 'signature' | 'drainage', @Query('tenantId') tenantId?: string, @Query('applicationId') applicationId?: string, @Query('keyword') keyword?: string, @Query('startAt') startAt?: string, @Query('endAt') endAt?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.listPending({ reportType, tenantId, applicationId, keyword, startAt, endAt, page: Number(page), pageSize: Number(pageSize), }); } @Get('templates/:reportType') async downloadTemplate( @Param('reportType') reportType: 'signature' | 'drainage', @CurrentSessionUserId() operatorId: string | undefined, @Res() response: DownloadResponse, ) { if (!['signature', 'drainage'].includes(reportType)) throw new BadRequestException('reportType must be signature or drainage'); this.sendWorkbook(response, await this.service.buildOfficialTemplate(reportType, operatorId)); } @Get('pending/export') async exportPending( @Query('reportType') reportType: 'signature' | 'drainage' | undefined, @Query('tenantId') tenantId: string | undefined, @Query('applicationId') applicationId: string | undefined, @CurrentSessionUserId() operatorId: string | undefined, @Res() response: DownloadResponse, ) { this.sendWorkbook(response, await this.service.exportPending({ reportType, tenantId, applicationId }, operatorId)); } @Get('import-profiles') listImportProfiles(@Query('reportType') reportType?: 'signature' | 'drainage') { return this.service.listImportProfiles(reportType); } @Post('import-profiles') @RequireRecentAuthentication() saveImportProfile(@Body() body: CreateImportProfileDto) { return this.service.saveImportProfile(body); } @Post('imports/analyze') @UseInterceptors( FileInterceptor('file', { limits: { fileSize: 100 * 1024 * 1024, files: 1, fields: 12, parts: 13 } }), ) analyzeImport( @UploadedFile() file: UploadedWorkbook, @Body() body: Record, @CurrentSessionUserId() operatorId?: string, ) { if (!file) throw new BadRequestException('请选择 XLSX 文件'); return this.service.analyzeImport(file, { tenantId: body.tenantId, applicationId: body.applicationId, reportType: body.reportType as 'signature' | 'drainage', sheetName: body.sheetName || undefined, headerRowCount: Number(body.headerRowCount || 1), dataStartRow: Number(body.dataStartRow || 2), profileId: body.profileId || undefined, operatorId, }); } @Put('imports/:id/commit') @RequireRecentAuthentication() commitImport(@Param('id') id: string, @Body() body: ImportCommitDto, @CurrentSessionUserId() operatorId?: string) { return this.service.commitImport(id, { ...body, operatorId }); } @Get('imports/review-batches') listImportReviewBatches( @Query('reportType') reportType?: 'signature' | 'drainage', @Query('status') status?: string, @Query('keyword') keyword?: string, @Query('startAt') startAt?: string, @Query('endAt') endAt?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.listImportReviewBatches({ reportType, status, keyword, startAt, endAt, page: Number(page), pageSize: Number(pageSize), }); } @Post('imports/:id/review') @RequireRecentAuthentication() reviewImportItems( @Param('id') id: string, @Body() body: ReviewImportItemsDto, @CurrentSessionUserId() reviewerId?: string, ) { return this.service.reviewImportItems(id, { ...body, reviewerId }); } @Get('batches') listBatches( @Query('keyword') keyword?: string, @Query('startAt') startAt?: string, @Query('endAt') endAt?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.listBatches({ keyword, startAt, endAt, page: Number(page), pageSize: Number(pageSize) }); } @Get('batches/:id/download') async downloadBatch( @Param('id') id: string, @Query('outputFormat') outputFormat: 'excel_drawing' | 'wps_cell_image' | undefined, @Res() response: DownloadResponse, ) { this.sendDownload(response, await this.batchDownloads.exportBundle(await this.service.getBatch(id), outputFormat)); } @Get('batches/:id/files/:fileId/download') async downloadBatchFile( @Param('id') id: string, @Param('fileId') fileId: string, @Query('outputFormat') outputFormat: 'excel_drawing' | 'wps_cell_image' | undefined, @Res() response: DownloadResponse, ) { this.sendDownload( response, await this.batchDownloads.exportFile(await this.service.getBatch(id), fileId, outputFormat), ); } @Get('batches/:id') getBatch(@Param('id') id: string) { return this.service.getBatch(id); } @Get('batches/:id/tasks') listBatchTasks( @Param('id') id: string, @Query('keyword') keyword?: string, @Query('reportType') reportType?: string, @Query('status') status?: string, @Query('channelId') channelId?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { return this.service.listBatchTasks(id, { keyword, reportType, status, channelId, page: Number(page), pageSize: Number(pageSize), }); } @Post('batches/preflight') preflightBatch(@Body() body: CreateReportBatchDto) { return this.service.preflightBatch(body); } @Post('batches') @RequireRecentAuthentication() createBatch(@Body() body: CreateReportBatchDto, @CurrentSessionUserId() operatorId?: string) { return this.service.createBatch({ ...body, createdById: operatorId }); } @Post('single-detail') getSingleMaterialDetail(@Body() body: SingleReportMaterialDto) { return this.service.getSingleMaterialDetail(body); } @Post('single-export') @RequireRecentAuthentication() async exportSingleMaterial( @Body() body: SingleReportMaterialDto | undefined, @CurrentSessionUserId() operatorId: string | undefined, @Res() response: DownloadResponse, ) { this.sendWorkbook(response, await this.service.exportSingleMaterial(body, operatorId)); } private sendWorkbook(response: DownloadResponse, exported: { fileName: string; content: Buffer }) { response.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); response.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(exported.fileName)}`); response.send(exported.content); } private sendDownload( response: DownloadResponse, exported: { fileName: string; contentType: string; content: Buffer }, ) { response.setHeader('Content-Type', exported.contentType); response.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(exported.fileName)}`); response.send(exported.content); } }