feat(batch-mining): 添加详细的调试日志

- mining-service batch-mining.service.ts: 添加所有方法的详细日志
- mining-admin-service batch-mining.service.ts: 添加 HTTP 请求和响应日志
- mining-admin-service batch-mining.controller.ts: 添加控制器层日志
- frontend batch-mining page.tsx: 添加前端 console.log 日志

便于调试部署后的 404 等问题

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-21 05:49:04 -08:00
parent cb9831f2fc
commit 7a4f5591b7
4 changed files with 237 additions and 70 deletions

View File

@ -8,6 +8,7 @@ import {
HttpStatus, HttpStatus,
UseInterceptors, UseInterceptors,
UploadedFile, UploadedFile,
Logger,
} from '@nestjs/common'; } from '@nestjs/common';
import { import {
ApiTags, ApiTags,
@ -24,12 +25,22 @@ import { BatchMiningService, BatchMiningItem } from '../../application/services/
@ApiBearerAuth() @ApiBearerAuth()
@Controller('batch-mining') @Controller('batch-mining')
export class BatchMiningController { export class BatchMiningController {
private readonly logger = new Logger(BatchMiningController.name);
constructor(private readonly batchMiningService: BatchMiningService) {} constructor(private readonly batchMiningService: BatchMiningService) {}
@Get('status') @Get('status')
@ApiOperation({ summary: '获取批量补发状态(是否已执行)' }) @ApiOperation({ summary: '获取批量补发状态(是否已执行)' })
async getStatus() { async getStatus() {
return this.batchMiningService.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') @Post('upload-preview')
@ -49,42 +60,56 @@ export class BatchMiningController {
}) })
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
async uploadAndPreview(@UploadedFile() file: Express.Multer.File) { async uploadAndPreview(@UploadedFile() file: Express.Multer.File) {
this.logger.log(`[POST /batch-mining/upload-preview] 开始处理上传预览请求`);
if (!file) { if (!file) {
this.logger.error(`[POST /batch-mining/upload-preview] 未收到文件`);
throw new HttpException('请上传文件', HttpStatus.BAD_REQUEST); throw new HttpException('请上传文件', HttpStatus.BAD_REQUEST);
} }
this.logger.log(`[POST /batch-mining/upload-preview] 收到文件: ${file.originalname}, 大小: ${file.size}, 类型: ${file.mimetype}`);
// 检查文件类型 // 检查文件类型
const validTypes = [ const validTypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel', 'application/vnd.ms-excel',
]; ];
if (!validTypes.includes(file.mimetype) && !file.originalname.endsWith('.xlsx')) { 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); throw new HttpException('请上传 Excel 文件 (.xlsx)', HttpStatus.BAD_REQUEST);
} }
try { try {
// 解析 Excel // 解析 Excel
this.logger.log(`[POST /batch-mining/upload-preview] 开始解析 Excel...`);
const workbook = XLSX.read(file.buffer, { type: 'buffer' }); 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 sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName]; const worksheet = workbook.Sheets[sheetName];
// 尝试读取 Sheet2如果存在 // 尝试读取 Sheet2如果存在
const actualSheet = workbook.SheetNames.includes('Sheet2') const actualSheetName = workbook.SheetNames.includes('Sheet2') ? 'Sheet2' : sheetName;
? workbook.Sheets['Sheet2'] const actualSheet = workbook.Sheets[actualSheetName];
: worksheet; this.logger.log(`[POST /batch-mining/upload-preview] 使用 Sheet: ${actualSheetName}`);
// 转换为数组 // 转换为数组
const rows: any[][] = XLSX.utils.sheet_to_json(actualSheet, { header: 1 }); 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); const items = this.batchMiningService.parseExcelData(rows);
this.logger.log(`[POST /batch-mining/upload-preview] 解析后有效数据: ${items.length}`);
if (items.length === 0) { if (items.length === 0) {
this.logger.error(`[POST /batch-mining/upload-preview] Excel 文件中没有有效数据`);
throw new HttpException('Excel 文件中没有有效数据', HttpStatus.BAD_REQUEST); throw new HttpException('Excel 文件中没有有效数据', HttpStatus.BAD_REQUEST);
} }
// 调用预览 API // 调用预览 API
this.logger.log(`[POST /batch-mining/upload-preview] 调用 mining-service 预览 API...`);
const preview = await this.batchMiningService.preview(items); const preview = await this.batchMiningService.preview(items);
this.logger.log(`[POST /batch-mining/upload-preview] 预览成功, 总金额: ${preview.grandTotalAmount}`);
return { return {
...preview, ...preview,
@ -95,6 +120,7 @@ export class BatchMiningController {
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;
} }
this.logger.error(`[POST /batch-mining/upload-preview] 解析 Excel 文件失败:`, error);
throw new HttpException( throw new HttpException(
`解析 Excel 文件失败: ${error instanceof Error ? error.message : error}`, `解析 Excel 文件失败: ${error instanceof Error ? error.message : error}`,
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@ -127,10 +153,21 @@ export class BatchMiningController {
}, },
}) })
async preview(@Body() body: { items: BatchMiningItem[] }) { async preview(@Body() body: { items: BatchMiningItem[] }) {
this.logger.log(`[POST /batch-mining/preview] 请求预览, 数据条数: ${body.items?.length || 0}`);
if (!body.items || body.items.length === 0) { if (!body.items || body.items.length === 0) {
this.logger.error(`[POST /batch-mining/preview] 数据为空`);
throw new HttpException('数据不能为空', HttpStatus.BAD_REQUEST); throw new HttpException('数据不能为空', HttpStatus.BAD_REQUEST);
} }
return this.batchMiningService.preview(body.items);
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') @Post('upload-execute')
@ -159,11 +196,17 @@ export class BatchMiningController {
@Body() body: { reason: string }, @Body() body: { reason: string },
@Req() req: any, @Req() req: any,
) { ) {
this.logger.log(`[POST /batch-mining/upload-execute] 开始处理上传执行请求`);
if (!file) { if (!file) {
this.logger.error(`[POST /batch-mining/upload-execute] 未收到文件`);
throw new HttpException('请上传文件', HttpStatus.BAD_REQUEST); 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) { if (!body.reason || body.reason.trim().length === 0) {
this.logger.error(`[POST /batch-mining/upload-execute] 补发原因为空`);
throw new HttpException('补发原因不能为空', HttpStatus.BAD_REQUEST); throw new HttpException('补发原因不能为空', HttpStatus.BAD_REQUEST);
} }
@ -173,31 +216,39 @@ export class BatchMiningController {
'application/vnd.ms-excel', 'application/vnd.ms-excel',
]; ];
if (!validTypes.includes(file.mimetype) && !file.originalname.endsWith('.xlsx')) { 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); throw new HttpException('请上传 Excel 文件 (.xlsx)', HttpStatus.BAD_REQUEST);
} }
try { try {
// 解析 Excel // 解析 Excel
this.logger.log(`[POST /batch-mining/upload-execute] 开始解析 Excel...`);
const workbook = XLSX.read(file.buffer, { type: 'buffer' }); const workbook = XLSX.read(file.buffer, { type: 'buffer' });
this.logger.log(`[POST /batch-mining/upload-execute] Excel Sheet 列表: ${workbook.SheetNames.join(', ')}`);
// 尝试读取 Sheet2如果存在 // 尝试读取 Sheet2如果存在
const actualSheet = workbook.SheetNames.includes('Sheet2') const actualSheetName = workbook.SheetNames.includes('Sheet2') ? 'Sheet2' : workbook.SheetNames[0];
? workbook.Sheets['Sheet2'] const actualSheet = workbook.Sheets[actualSheetName];
: workbook.Sheets[workbook.SheetNames[0]]; this.logger.log(`[POST /batch-mining/upload-execute] 使用 Sheet: ${actualSheetName}`);
// 转换为数组 // 转换为数组
const rows: any[][] = XLSX.utils.sheet_to_json(actualSheet, { header: 1 }); 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); const items = this.batchMiningService.parseExcelData(rows);
this.logger.log(`[POST /batch-mining/upload-execute] 解析后有效数据: ${items.length}`);
if (items.length === 0) { if (items.length === 0) {
this.logger.error(`[POST /batch-mining/upload-execute] Excel 文件中没有有效数据`);
throw new HttpException('Excel 文件中没有有效数据', HttpStatus.BAD_REQUEST); throw new HttpException('Excel 文件中没有有效数据', HttpStatus.BAD_REQUEST);
} }
const admin = req.admin; const admin = req.admin;
this.logger.log(`[POST /batch-mining/upload-execute] 操作管理员: ${admin?.username} (${admin?.id})`);
// 调用执行 API // 调用执行 API
this.logger.log(`[POST /batch-mining/upload-execute] 调用 mining-service 执行 API...`);
const result = await this.batchMiningService.execute( const result = await this.batchMiningService.execute(
{ {
items, items,
@ -208,6 +259,8 @@ export class BatchMiningController {
admin.id, admin.id,
); );
this.logger.log(`[POST /batch-mining/upload-execute] 执行成功: successCount=${result.successCount}, totalAmount=${result.totalAmount}`);
return { return {
...result, ...result,
originalFileName: file.originalname, originalFileName: file.originalname,
@ -216,6 +269,7 @@ export class BatchMiningController {
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;
} }
this.logger.error(`[POST /batch-mining/upload-execute] 执行失败:`, error);
throw new HttpException( throw new HttpException(
`执行失败: ${error instanceof Error ? error.message : error}`, `执行失败: ${error instanceof Error ? error.message : error}`,
HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST,
@ -252,34 +306,59 @@ export class BatchMiningController {
@Body() body: { items: BatchMiningItem[]; reason: string }, @Body() body: { items: BatchMiningItem[]; reason: string },
@Req() req: any, @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) { if (!body.items || body.items.length === 0) {
this.logger.error(`[POST /batch-mining/execute] 数据为空`);
throw new HttpException('数据不能为空', HttpStatus.BAD_REQUEST); throw new HttpException('数据不能为空', HttpStatus.BAD_REQUEST);
} }
if (!body.reason || body.reason.trim().length === 0) { if (!body.reason || body.reason.trim().length === 0) {
this.logger.error(`[POST /batch-mining/execute] 补发原因为空`);
throw new HttpException('补发原因不能为空', HttpStatus.BAD_REQUEST); throw new HttpException('补发原因不能为空', HttpStatus.BAD_REQUEST);
} }
const admin = req.admin; const admin = req.admin;
this.logger.log(`[POST /batch-mining/execute] 操作管理员: ${admin?.username} (${admin?.id})`);
return this.batchMiningService.execute( try {
{ const result = await this.batchMiningService.execute(
items: body.items, {
operatorId: admin.id, items: body.items,
operatorName: admin.username, operatorId: admin.id,
reason: body.reason, operatorName: admin.username,
}, reason: body.reason,
admin.id, },
); 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') @Get('execution')
@ApiOperation({ summary: '获取批量补发执行记录(含明细)' }) @ApiOperation({ summary: '获取批量补发执行记录(含明细)' })
async getExecution() { async getExecution() {
const execution = await this.batchMiningService.getExecution(); this.logger.log(`[GET /batch-mining/execution] 请求获取执行记录`);
if (!execution) {
throw new HttpException('尚未执行过批量补发', HttpStatus.NOT_FOUND); 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;
} }
return execution;
} }
} }

View File

@ -47,30 +47,35 @@ export class BatchMiningService {
* *
*/ */
async getStatus(): Promise<any> { async getStatus(): Promise<any> {
try { const url = `${this.miningServiceUrl}/admin/batch-mining/status`;
const response = await fetch( this.logger.log(`[getStatus] 开始获取批量补发状态, URL: ${url}`);
`${this.miningServiceUrl}/admin/batch-mining/status`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
);
try {
this.logger.log(`[getStatus] 发送 GET 请求...`);
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
this.logger.log(`[getStatus] 响应状态码: ${response.status}`);
const result = await response.json(); const result = await response.json();
this.logger.log(`[getStatus] 响应数据: ${JSON.stringify(result)}`);
if (!response.ok) { if (!response.ok) {
this.logger.error(`[getStatus] 请求失败: ${result.message || '未知错误'}`);
throw new HttpException( throw new HttpException(
result.message || '获取状态失败', result.message || '获取状态失败',
response.status, response.status,
); );
} }
this.logger.log(`[getStatus] 成功获取状态: hasExecuted=${result.hasExecuted}`);
return result; return result;
} catch (error) { } catch (error) {
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;
} }
this.logger.error('Failed to get batch mining status', error); this.logger.error(`[getStatus] 调用 mining-service 失败:`, error);
throw new HttpException( throw new HttpException(
`调用 mining-service 失败: ${error instanceof Error ? error.message : error}`, `调用 mining-service 失败: ${error instanceof Error ? error.message : error}`,
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -82,31 +87,38 @@ export class BatchMiningService {
* *
*/ */
async preview(items: BatchMiningItem[]): Promise<any> { async preview(items: BatchMiningItem[]): Promise<any> {
try { const url = `${this.miningServiceUrl}/admin/batch-mining/preview`;
const response = await fetch( this.logger.log(`[preview] 开始预览批量补发, URL: ${url}`);
`${this.miningServiceUrl}/admin/batch-mining/preview`, this.logger.log(`[preview] 数据条数: ${items.length}`);
{ this.logger.log(`[preview] 前3条数据: ${JSON.stringify(items.slice(0, 3))}`);
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
},
);
try {
this.logger.log(`[preview] 发送 POST 请求...`);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
});
this.logger.log(`[preview] 响应状态码: ${response.status}`);
const result = await response.json(); const result = await response.json();
this.logger.log(`[preview] 响应数据概要: totalBatches=${result.totalBatches}, totalUsers=${result.totalUsers}, grandTotalAmount=${result.grandTotalAmount}`);
if (!response.ok) { if (!response.ok) {
this.logger.error(`[preview] 请求失败: ${result.message || '未知错误'}`);
throw new HttpException( throw new HttpException(
result.message || '预览失败', result.message || '预览失败',
response.status, response.status,
); );
} }
this.logger.log(`[preview] 预览成功`);
return result; return result;
} catch (error) { } catch (error) {
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;
} }
this.logger.error('Failed to preview batch mining', error); this.logger.error(`[preview] 调用 mining-service 失败:`, error);
throw new HttpException( throw new HttpException(
`调用 mining-service 失败: ${error instanceof Error ? error.message : error}`, `调用 mining-service 失败: ${error instanceof Error ? error.message : error}`,
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -121,19 +133,26 @@ export class BatchMiningService {
request: BatchMiningRequest, request: BatchMiningRequest,
adminId: string, adminId: string,
): Promise<any> { ): Promise<any> {
try { const url = `${this.miningServiceUrl}/admin/batch-mining/execute`;
const response = await fetch( this.logger.log(`[execute] 开始执行批量补发, URL: ${url}`);
`${this.miningServiceUrl}/admin/batch-mining/execute`, this.logger.log(`[execute] 操作人: ${request.operatorName} (${request.operatorId})`);
{ this.logger.log(`[execute] 原因: ${request.reason}`);
method: 'POST', this.logger.log(`[execute] 数据条数: ${request.items.length}`);
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
},
);
try {
this.logger.log(`[execute] 发送 POST 请求...`);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
});
this.logger.log(`[execute] 响应状态码: ${response.status}`);
const result = await response.json(); const result = await response.json();
this.logger.log(`[execute] 响应数据: ${JSON.stringify(result)}`);
if (!response.ok) { if (!response.ok) {
this.logger.error(`[execute] 请求失败: ${result.message || '未知错误'}`);
throw new HttpException( throw new HttpException(
result.message || '执行失败', result.message || '执行失败',
response.status, response.status,
@ -141,6 +160,7 @@ export class BatchMiningService {
} }
// 记录审计日志 // 记录审计日志
this.logger.log(`[execute] 记录审计日志...`);
await this.prisma.auditLog.create({ await this.prisma.auditLog.create({
data: { data: {
adminId, adminId,
@ -158,7 +178,7 @@ export class BatchMiningService {
}); });
this.logger.log( this.logger.log(
`Batch mining executed by admin ${adminId}: total=${result.totalUsers}, success=${result.successCount}, amount=${result.totalAmount}`, `[execute] 批量补发执行成功: admin=${adminId}, total=${result.totalUsers}, success=${result.successCount}, amount=${result.totalAmount}`,
); );
return result; return result;
@ -166,7 +186,7 @@ export class BatchMiningService {
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;
} }
this.logger.error('Failed to execute batch mining', error); this.logger.error(`[execute] 调用 mining-service 失败:`, error);
throw new HttpException( throw new HttpException(
`调用 mining-service 失败: ${error instanceof Error ? error.message : error}`, `调用 mining-service 失败: ${error instanceof Error ? error.message : error}`,
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -178,34 +198,41 @@ export class BatchMiningService {
* *
*/ */
async getExecution(): Promise<any> { async getExecution(): Promise<any> {
const url = `${this.miningServiceUrl}/admin/batch-mining/execution`;
this.logger.log(`[getExecution] 开始获取执行记录, URL: ${url}`);
try { try {
const response = await fetch( this.logger.log(`[getExecution] 发送 GET 请求...`);
`${this.miningServiceUrl}/admin/batch-mining/execution`, const response = await fetch(url, {
{ method: 'GET',
method: 'GET', headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, });
},
); this.logger.log(`[getExecution] 响应状态码: ${response.status}`);
if (response.status === 404) { if (response.status === 404) {
this.logger.log(`[getExecution] 未找到执行记录 (404)`);
return null; return null;
} }
const result = await response.json(); const result = await response.json();
this.logger.log(`[getExecution] 响应数据概要: id=${result.id}, totalUsers=${result.totalUsers}`);
if (!response.ok) { if (!response.ok) {
this.logger.error(`[getExecution] 请求失败: ${result.message || '未知错误'}`);
throw new HttpException( throw new HttpException(
result.message || '获取记录失败', result.message || '获取记录失败',
response.status, response.status,
); );
} }
this.logger.log(`[getExecution] 成功获取执行记录`);
return result; return result;
} catch (error) { } catch (error) {
if (error instanceof HttpException) { if (error instanceof HttpException) {
throw error; throw error;
} }
this.logger.error('Failed to get batch mining execution', error); this.logger.error(`[getExecution] 调用 mining-service 失败:`, error);
throw new HttpException( throw new HttpException(
`调用 mining-service 失败: ${error instanceof Error ? error.message : error}`, `调用 mining-service 失败: ${error instanceof Error ? error.message : error}`,
HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR,
@ -219,6 +246,7 @@ export class BatchMiningService {
* | ID | | | | | * | ID | | | | |
*/ */
parseExcelData(rows: any[]): BatchMiningItem[] { parseExcelData(rows: any[]): BatchMiningItem[] {
this.logger.log(`[parseExcelData] 开始解析 Excel 数据, 总行数: ${rows.length}`);
const items: BatchMiningItem[] = []; const items: BatchMiningItem[] = [];
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
@ -226,12 +254,14 @@ export class BatchMiningService {
// 跳过标题行和汇总行 // 跳过标题行和汇总行
if (!row || typeof row[1] !== 'string' || row[1] === '注册ID' || row[1] === '合计') { if (!row || typeof row[1] !== 'string' || row[1] === '注册ID' || row[1] === '合计') {
this.logger.debug(`[parseExcelData] 跳过行 ${i + 1}: 标题行或汇总行`);
continue; continue;
} }
// 跳过认种量为 0 或无效的行 // 跳过认种量为 0 或无效的行
const treeCount = parseInt(row[2], 10); const treeCount = parseInt(row[2], 10);
if (isNaN(treeCount) || treeCount <= 0) { if (isNaN(treeCount) || treeCount <= 0) {
this.logger.debug(`[parseExcelData] 跳过行 ${i + 1}: 认种量无效 (${row[2]})`);
continue; continue;
} }
@ -245,7 +275,7 @@ export class BatchMiningService {
const preMineDays = parseInt(row[5], 10); const preMineDays = parseInt(row[5], 10);
if (isNaN(batch) || isNaN(preMineDays) || preMineDays <= 0) { if (isNaN(batch) || isNaN(preMineDays) || preMineDays <= 0) {
this.logger.warn(`Skipping row ${i + 1}: invalid batch or preMineDays`); this.logger.warn(`[parseExcelData] 跳过行 ${i + 1}: 批次或提前天数无效 (batch=${row[4]}, preMineDays=${row[5]})`);
continue; continue;
} }
@ -259,6 +289,12 @@ export class BatchMiningService {
}); });
} }
this.logger.log(`[parseExcelData] 解析完成, 有效数据: ${items.length}`);
if (items.length > 0) {
this.logger.log(`[parseExcelData] 第一条数据: ${JSON.stringify(items[0])}`);
this.logger.log(`[parseExcelData] 最后一条数据: ${JSON.stringify(items[items.length - 1])}`);
}
return items; return items;
} }
} }

View File

@ -112,7 +112,9 @@ export class BatchMiningService {
* *
*/ */
async hasExecuted(): Promise<boolean> { async hasExecuted(): Promise<boolean> {
this.logger.log('[hasExecuted] 检查批量补发是否已执行...');
const record = await this.prisma.batchMiningExecution.findFirst(); const record = await this.prisma.batchMiningExecution.findFirst();
this.logger.log(`[hasExecuted] 结果: ${!!record}`);
return !!record; return !!record;
} }
@ -120,6 +122,9 @@ export class BatchMiningService {
* *
*/ */
async preview(items: BatchMiningItem[]): Promise<BatchMiningPreviewResult> { async preview(items: BatchMiningItem[]): Promise<BatchMiningPreviewResult> {
this.logger.log(`[preview] 开始预览批量补发, 共 ${items.length} 条数据`);
this.logger.debug(`[preview] 数据样例: ${JSON.stringify(items.slice(0, 3))}`);
// 检查是否已执行过 // 检查是否已执行过
const alreadyExecuted = await this.hasExecuted(); const alreadyExecuted = await this.hasExecuted();
if (alreadyExecuted) { if (alreadyExecuted) {
@ -136,16 +141,20 @@ export class BatchMiningService {
} }
// 获取挖矿配置 // 获取挖矿配置
this.logger.log('[preview] 获取挖矿配置...');
const config = await this.miningConfigRepository.getConfig(); const config = await this.miningConfigRepository.getConfig();
if (!config) { if (!config) {
this.logger.error('[preview] 挖矿配置不存在');
throw new BadRequestException('挖矿配置不存在'); throw new BadRequestException('挖矿配置不存在');
} }
const secondDistribution = config.secondDistribution.value; const secondDistribution = config.secondDistribution.value;
this.logger.log(`[preview] 每秒分配量: ${secondDistribution.toString()}`);
// 按批次分组并排序 // 按批次分组并排序
const batchGroups = this.groupByBatch(items); const batchGroups = this.groupByBatch(items);
const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b); const sortedBatches = Array.from(batchGroups.keys()).sort((a, b) => a - b);
this.logger.log(`[preview] 分组完成, 共 ${sortedBatches.length} 个批次: ${sortedBatches.join(', ')}`);
let cumulativeContribution = new Decimal(0); // 累计全网算力 let cumulativeContribution = new Decimal(0); // 累计全网算力
let grandTotalAmount = new Decimal(0); let grandTotalAmount = new Decimal(0);
@ -199,7 +208,7 @@ export class BatchMiningService {
grandTotalAmount = grandTotalAmount.plus(batchTotalAmount); grandTotalAmount = grandTotalAmount.plus(batchTotalAmount);
} }
return { const result = {
canExecute: true, canExecute: true,
alreadyExecuted: false, alreadyExecuted: false,
totalBatches: sortedBatches.length, totalBatches: sortedBatches.length,
@ -208,6 +217,8 @@ export class BatchMiningService {
grandTotalAmount: grandTotalAmount.toFixed(8), grandTotalAmount: grandTotalAmount.toFixed(8),
message: `预览成功: ${sortedBatches.length} 个批次, ${items.length} 个用户, 总补发金额 ${grandTotalAmount.toFixed(8)}`, message: `预览成功: ${sortedBatches.length} 个批次, ${items.length} 个用户, 总补发金额 ${grandTotalAmount.toFixed(8)}`,
}; };
this.logger.log(`[preview] 预览完成: ${result.message}`);
return result;
} }
/** /**
@ -215,6 +226,8 @@ export class BatchMiningService {
*/ */
async execute(request: BatchMiningRequest): Promise<BatchMiningResult> { async execute(request: BatchMiningRequest): Promise<BatchMiningResult> {
const { items, operatorId, operatorName, reason } = request; const { items, operatorId, operatorName, reason } = request;
this.logger.log(`[execute] 开始执行批量补发, 操作人: ${operatorName}, 共 ${items.length} 条数据`);
this.logger.log(`[execute] 原因: ${reason}`);
// 检查是否已执行过 // 检查是否已执行过
const alreadyExecuted = await this.hasExecuted(); const alreadyExecuted = await this.hasExecuted();
@ -446,6 +459,7 @@ export class BatchMiningService {
* *
*/ */
async getExecution(): Promise<any | null> { async getExecution(): Promise<any | null> {
this.logger.log('[getExecution] 获取批量补发执行记录...');
const execution = await this.prisma.batchMiningExecution.findFirst({ const execution = await this.prisma.batchMiningExecution.findFirst({
include: { include: {
records: { records: {
@ -455,9 +469,11 @@ export class BatchMiningService {
}); });
if (!execution) { if (!execution) {
this.logger.log('[getExecution] 未找到执行记录');
return null; return null;
} }
this.logger.log(`[getExecution] 找到执行记录: id=${execution.id}, records=${execution.records.length}`);
return { return {
id: execution.id, id: execution.id,
operatorId: execution.operatorId, operatorId: execution.operatorId,

View File

@ -110,8 +110,15 @@ export default function BatchMiningPage() {
const { data: statusData, isLoading: statusLoading } = useQuery({ const { data: statusData, isLoading: statusLoading } = useQuery({
queryKey: ['batch-mining-status'], queryKey: ['batch-mining-status'],
queryFn: async () => { queryFn: async () => {
const res = await apiClient.get('/batch-mining/status'); console.log('[BatchMining] 开始获取批量补发状态...');
return res.data; try {
const res = await apiClient.get('/batch-mining/status');
console.log('[BatchMining] 状态响应:', res.data);
return res.data;
} catch (error) {
console.error('[BatchMining] 获取状态失败:', error);
throw error;
}
}, },
}); });
@ -119,10 +126,13 @@ export default function BatchMiningPage() {
const { data: executionData, isLoading: executionLoading } = useQuery({ const { data: executionData, isLoading: executionLoading } = useQuery({
queryKey: ['batch-mining-execution'], queryKey: ['batch-mining-execution'],
queryFn: async () => { queryFn: async () => {
console.log('[BatchMining] 开始获取执行记录...');
try { try {
const res = await apiClient.get('/batch-mining/execution'); const res = await apiClient.get('/batch-mining/execution');
console.log('[BatchMining] 执行记录响应:', res.data);
return res.data as BatchExecution; return res.data as BatchExecution;
} catch { } catch (error) {
console.error('[BatchMining] 获取执行记录失败:', error);
return null; return null;
} }
}, },
@ -132,22 +142,32 @@ export default function BatchMiningPage() {
// 上传预览 // 上传预览
const uploadPreviewMutation = useMutation({ const uploadPreviewMutation = useMutation({
mutationFn: async (file: File) => { mutationFn: async (file: File) => {
console.log('[BatchMining] 开始上传文件预览:', file.name, '大小:', file.size);
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
const res = await apiClient.post('/batch-mining/upload-preview', formData, { try {
headers: { 'Content-Type': 'multipart/form-data' }, const res = await apiClient.post('/batch-mining/upload-preview', formData, {
}); headers: { 'Content-Type': 'multipart/form-data' },
return res.data as BatchPreviewResult; });
console.log('[BatchMining] 上传预览响应:', res.data);
return res.data as BatchPreviewResult;
} catch (error: any) {
console.error('[BatchMining] 上传预览失败:', error.response?.status, error.response?.data);
throw error;
}
}, },
onSuccess: (data) => { onSuccess: (data) => {
console.log('[BatchMining] 预览成功, 批次数:', data.totalBatches, '用户数:', data.totalUsers, '总金额:', data.grandTotalAmount);
setPreviewResult(data); setPreviewResult(data);
setParsedItems(data.parsedItems || []); setParsedItems(data.parsedItems || []);
setFileName(data.originalFileName || ''); setFileName(data.originalFileName || '');
if (data.alreadyExecuted) { if (data.alreadyExecuted) {
console.warn('[BatchMining] 批量补发已执行过');
toast({ title: '批量补发已执行过,不能重复执行', variant: 'destructive' }); toast({ title: '批量补发已执行过,不能重复执行', variant: 'destructive' });
} }
}, },
onError: (error: any) => { onError: (error: any) => {
console.error('[BatchMining] 上传预览 mutation 错误:', error);
toast({ title: error.response?.data?.message || '上传失败', variant: 'destructive' }); toast({ title: error.response?.data?.message || '上传失败', variant: 'destructive' });
setPreviewResult(null); setPreviewResult(null);
setParsedItems([]); setParsedItems([]);
@ -157,10 +177,18 @@ export default function BatchMiningPage() {
// 执行补发 // 执行补发
const executeMutation = useMutation({ const executeMutation = useMutation({
mutationFn: async (data: { items: BatchItem[]; reason: string }) => { mutationFn: async (data: { items: BatchItem[]; reason: string }) => {
const res = await apiClient.post('/batch-mining/execute', data); console.log('[BatchMining] 开始执行批量补发, 数据条数:', data.items.length, '原因:', data.reason);
return res.data; try {
const res = await apiClient.post('/batch-mining/execute', data);
console.log('[BatchMining] 执行响应:', res.data);
return res.data;
} catch (error: any) {
console.error('[BatchMining] 执行失败:', error.response?.status, error.response?.data);
throw error;
}
}, },
onSuccess: (data) => { onSuccess: (data) => {
console.log('[BatchMining] 执行成功, successCount:', data.successCount, 'totalAmount:', data.totalAmount);
toast({ toast({
title: `批量补发成功`, title: `批量补发成功`,
description: `成功 ${data.successCount} 个,总金额 ${parseFloat(data.totalAmount).toFixed(8)} 积分股`, description: `成功 ${data.successCount} 个,总金额 ${parseFloat(data.totalAmount).toFixed(8)} 积分股`,
@ -175,14 +203,19 @@ export default function BatchMiningPage() {
queryClient.invalidateQueries({ queryKey: ['batch-mining-execution'] }); queryClient.invalidateQueries({ queryKey: ['batch-mining-execution'] });
}, },
onError: (error: any) => { onError: (error: any) => {
console.error('[BatchMining] 执行 mutation 错误:', error);
toast({ title: error.response?.data?.message || '执行失败', variant: 'destructive' }); toast({ title: error.response?.data?.message || '执行失败', variant: 'destructive' });
}, },
}); });
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => { const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log('[BatchMining] 文件选择事件触发');
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
console.log('[BatchMining] 选中文件:', file.name, '大小:', file.size, '类型:', file.type);
uploadPreviewMutation.mutate(file); uploadPreviewMutation.mutate(file);
} else {
console.log('[BatchMining] 未选择文件');
} }
// Reset input // Reset input
if (fileInputRef.current) { if (fileInputRef.current) {
@ -191,10 +224,13 @@ export default function BatchMiningPage() {
}; };
const handleExecute = () => { const handleExecute = () => {
console.log('[BatchMining] 点击执行按钮, 原因:', reason, '数据条数:', parsedItems.length);
if (!reason.trim()) { if (!reason.trim()) {
console.warn('[BatchMining] 补发原因为空');
toast({ title: '请输入补发原因', variant: 'destructive' }); toast({ title: '请输入补发原因', variant: 'destructive' });
return; return;
} }
console.log('[BatchMining] 开始调用执行 mutation...');
executeMutation.mutate({ items: parsedItems, reason }); executeMutation.mutate({ items: parsedItems, reason });
}; };