import { Controller, Get, Post, Body, Req, HttpException, HttpStatus, UseInterceptors, UploadedFile, Logger, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiConsumes, } from '@nestjs/swagger'; import { FileInterceptor } from '@nestjs/platform-express'; import * as XLSX from 'xlsx'; import { BatchMiningService, BatchMiningItem } from '../../application/services/batch-mining.service'; @ApiTags('Batch Mining') @ApiBearerAuth() @Controller('batch-mining') export class BatchMiningController { private readonly logger = new Logger(BatchMiningController.name); constructor(private readonly batchMiningService: BatchMiningService) {} @Get('status') @ApiOperation({ summary: '获取批量补发状态(是否已执行)' }) async getStatus() { this.logger.log(`[GET /batch-mining/status] 请求获取批量补发状态`); try { const result = await this.batchMiningService.getStatus(); this.logger.log(`[GET /batch-mining/status] 返回: ${JSON.stringify(result)}`); return result; } catch (error) { this.logger.error(`[GET /batch-mining/status] 错误:`, error); throw error; } } @Post('upload-preview') @ApiOperation({ summary: '上传 Excel 文件并预览(不执行)' }) @ApiConsumes('multipart/form-data') @ApiBody({ schema: { type: 'object', properties: { file: { type: 'string', format: 'binary', description: 'Excel 文件 (.xlsx)', }, }, }, }) @UseInterceptors(FileInterceptor('file')) async uploadAndPreview(@UploadedFile() file: Express.Multer.File) { this.logger.log(`[POST /batch-mining/upload-preview] 开始处理上传预览请求`); if (!file) { this.logger.error(`[POST /batch-mining/upload-preview] 未收到文件`); throw new HttpException('请上传文件', HttpStatus.BAD_REQUEST); } this.logger.log(`[POST /batch-mining/upload-preview] 收到文件: ${file.originalname}, 大小: ${file.size}, 类型: ${file.mimetype}`); // 检查文件类型 const validTypes = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', ]; if (!validTypes.includes(file.mimetype) && !file.originalname.endsWith('.xlsx')) { this.logger.error(`[POST /batch-mining/upload-preview] 文件类型不正确: ${file.mimetype}`); throw new HttpException('请上传 Excel 文件 (.xlsx)', HttpStatus.BAD_REQUEST); } try { // 解析 Excel this.logger.log(`[POST /batch-mining/upload-preview] 开始解析 Excel...`); const workbook = XLSX.read(file.buffer, { type: 'buffer' }); this.logger.log(`[POST /batch-mining/upload-preview] Excel Sheet 列表: ${workbook.SheetNames.join(', ')}`); const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; // 尝试读取 Sheet2(如果存在) const actualSheetName = workbook.SheetNames.includes('Sheet2') ? 'Sheet2' : sheetName; const actualSheet = workbook.Sheets[actualSheetName]; this.logger.log(`[POST /batch-mining/upload-preview] 使用 Sheet: ${actualSheetName}`); // 转换为数组 const rows: any[][] = XLSX.utils.sheet_to_json(actualSheet, { header: 1 }); this.logger.log(`[POST /batch-mining/upload-preview] Excel 总行数: ${rows.length}`); // 解析数据 const items = this.batchMiningService.parseExcelData(rows); this.logger.log(`[POST /batch-mining/upload-preview] 解析后有效数据: ${items.length} 条`); if (items.length === 0) { this.logger.error(`[POST /batch-mining/upload-preview] Excel 文件中没有有效数据`); throw new HttpException('Excel 文件中没有有效数据', HttpStatus.BAD_REQUEST); } // 调用预览 API this.logger.log(`[POST /batch-mining/upload-preview] 调用 mining-service 预览 API...`); const preview = await this.batchMiningService.preview(items); this.logger.log(`[POST /batch-mining/upload-preview] 预览成功, 总金额: ${preview.grandTotalAmount}`); return { ...preview, parsedItems: items, originalFileName: file.originalname, }; } catch (error) { if (error instanceof HttpException) { throw error; } this.logger.error(`[POST /batch-mining/upload-preview] 解析 Excel 文件失败:`, error); throw new HttpException( `解析 Excel 文件失败: ${error instanceof Error ? error.message : error}`, HttpStatus.BAD_REQUEST, ); } } @Post('preview') @ApiOperation({ summary: '预览批量补发(传入解析后的数据)' }) @ApiBody({ schema: { type: 'object', required: ['items'], properties: { items: { type: 'array', items: { type: 'object', properties: { accountSequence: { type: 'string' }, treeCount: { type: 'number' }, miningStartDate: { type: 'string' }, batch: { type: 'number' }, preMineDays: { type: 'number' }, remark: { type: 'string' }, }, }, }, }, }, }) async preview(@Body() body: { items: BatchMiningItem[] }) { this.logger.log(`[POST /batch-mining/preview] 请求预览, 数据条数: ${body.items?.length || 0}`); if (!body.items || body.items.length === 0) { this.logger.error(`[POST /batch-mining/preview] 数据为空`); throw new HttpException('数据不能为空', HttpStatus.BAD_REQUEST); } try { const result = await this.batchMiningService.preview(body.items); this.logger.log(`[POST /batch-mining/preview] 预览成功`); return result; } catch (error) { this.logger.error(`[POST /batch-mining/preview] 错误:`, error); throw error; } } @Post('upload-execute') @ApiOperation({ summary: '上传 Excel 文件并执行批量补发(只能执行一次)' }) @ApiConsumes('multipart/form-data') @ApiBody({ schema: { type: 'object', required: ['file', 'reason'], properties: { file: { type: 'string', format: 'binary', description: 'Excel 文件 (.xlsx)', }, reason: { type: 'string', description: '补发原因(必填)', }, }, }, }) @UseInterceptors(FileInterceptor('file')) async uploadAndExecute( @UploadedFile() file: Express.Multer.File, @Body() body: { reason: string }, @Req() req: any, ) { this.logger.log(`[POST /batch-mining/upload-execute] 开始处理上传执行请求`); if (!file) { this.logger.error(`[POST /batch-mining/upload-execute] 未收到文件`); throw new HttpException('请上传文件', HttpStatus.BAD_REQUEST); } this.logger.log(`[POST /batch-mining/upload-execute] 收到文件: ${file.originalname}, 原因: ${body.reason}`); if (!body.reason || body.reason.trim().length === 0) { this.logger.error(`[POST /batch-mining/upload-execute] 补发原因为空`); throw new HttpException('补发原因不能为空', HttpStatus.BAD_REQUEST); } // 检查文件类型 const validTypes = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', ]; if (!validTypes.includes(file.mimetype) && !file.originalname.endsWith('.xlsx')) { this.logger.error(`[POST /batch-mining/upload-execute] 文件类型不正确: ${file.mimetype}`); throw new HttpException('请上传 Excel 文件 (.xlsx)', HttpStatus.BAD_REQUEST); } try { // 解析 Excel this.logger.log(`[POST /batch-mining/upload-execute] 开始解析 Excel...`); const workbook = XLSX.read(file.buffer, { type: 'buffer' }); this.logger.log(`[POST /batch-mining/upload-execute] Excel Sheet 列表: ${workbook.SheetNames.join(', ')}`); // 尝试读取 Sheet2(如果存在) const actualSheetName = workbook.SheetNames.includes('Sheet2') ? 'Sheet2' : workbook.SheetNames[0]; const actualSheet = workbook.Sheets[actualSheetName]; this.logger.log(`[POST /batch-mining/upload-execute] 使用 Sheet: ${actualSheetName}`); // 转换为数组 const rows: any[][] = XLSX.utils.sheet_to_json(actualSheet, { header: 1 }); this.logger.log(`[POST /batch-mining/upload-execute] Excel 总行数: ${rows.length}`); // 解析数据 const items = this.batchMiningService.parseExcelData(rows); this.logger.log(`[POST /batch-mining/upload-execute] 解析后有效数据: ${items.length} 条`); if (items.length === 0) { this.logger.error(`[POST /batch-mining/upload-execute] Excel 文件中没有有效数据`); throw new HttpException('Excel 文件中没有有效数据', HttpStatus.BAD_REQUEST); } const admin = req.admin; this.logger.log(`[POST /batch-mining/upload-execute] 操作管理员: ${admin?.username} (${admin?.id})`); // 调用执行 API this.logger.log(`[POST /batch-mining/upload-execute] 调用 mining-service 执行 API...`); const result = await this.batchMiningService.execute( { items, operatorId: admin.id, operatorName: admin.username, reason: body.reason, }, admin.id, ); this.logger.log(`[POST /batch-mining/upload-execute] 执行成功: successCount=${result.successCount}, totalAmount=${result.totalAmount}`); return { ...result, originalFileName: file.originalname, }; } catch (error) { if (error instanceof HttpException) { throw error; } this.logger.error(`[POST /batch-mining/upload-execute] 执行失败:`, error); throw new HttpException( `执行失败: ${error instanceof Error ? error.message : error}`, HttpStatus.BAD_REQUEST, ); } } @Post('execute') @ApiOperation({ summary: '执行批量补发(传入解析后的数据,只能执行一次)' }) @ApiBody({ schema: { type: 'object', required: ['items', 'reason'], properties: { items: { type: 'array', items: { type: 'object', properties: { accountSequence: { type: 'string' }, treeCount: { type: 'number' }, miningStartDate: { type: 'string' }, batch: { type: 'number' }, preMineDays: { type: 'number' }, remark: { type: 'string' }, }, }, }, reason: { type: 'string', description: '补发原因(必填)' }, }, }, }) async execute( @Body() body: { items: BatchMiningItem[]; reason: string }, @Req() req: any, ) { this.logger.log(`[POST /batch-mining/execute] 请求执行批量补发`); this.logger.log(`[POST /batch-mining/execute] 数据条数: ${body.items?.length || 0}, 原因: ${body.reason}`); if (!body.items || body.items.length === 0) { this.logger.error(`[POST /batch-mining/execute] 数据为空`); throw new HttpException('数据不能为空', HttpStatus.BAD_REQUEST); } if (!body.reason || body.reason.trim().length === 0) { this.logger.error(`[POST /batch-mining/execute] 补发原因为空`); throw new HttpException('补发原因不能为空', HttpStatus.BAD_REQUEST); } const admin = req.admin; this.logger.log(`[POST /batch-mining/execute] 操作管理员: ${admin?.username} (${admin?.id})`); try { const result = await this.batchMiningService.execute( { items: body.items, operatorId: admin.id, operatorName: admin.username, reason: body.reason, }, admin.id, ); this.logger.log(`[POST /batch-mining/execute] 执行成功`); return result; } catch (error) { this.logger.error(`[POST /batch-mining/execute] 错误:`, error); throw error; } } @Get('execution') @ApiOperation({ summary: '获取批量补发执行记录(含明细)' }) async getExecution() { this.logger.log(`[GET /batch-mining/execution] 请求获取执行记录`); try { const execution = await this.batchMiningService.getExecution(); if (!execution) { this.logger.log(`[GET /batch-mining/execution] 尚未执行过批量补发`); throw new HttpException('尚未执行过批量补发', HttpStatus.NOT_FOUND); } this.logger.log(`[GET /batch-mining/execution] 返回执行记录: id=${execution.id}`); return execution; } catch (error) { if (error instanceof HttpException) { throw error; } this.logger.error(`[GET /batch-mining/execution] 错误:`, error); throw error; } } }