rwadurian/backend/services/mining-admin-service/src/api/controllers/batch-mining.controller.ts

365 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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