365 lines
13 KiB
TypeScript
365 lines
13 KiB
TypeScript
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;
|
||
}
|
||
}
|
||
}
|