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,
UseInterceptors,
UploadedFile,
Logger,
} from '@nestjs/common';
import {
ApiTags,
@ -24,12 +25,22 @@ import { BatchMiningService, BatchMiningItem } from '../../application/services/
@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() {
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')
@ -49,42 +60,56 @@ export class BatchMiningController {
})
@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 actualSheet = workbook.SheetNames.includes('Sheet2')
? workbook.Sheets['Sheet2']
: worksheet;
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,
@ -95,6 +120,7 @@ export class BatchMiningController {
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,
@ -127,10 +153,21 @@ export class BatchMiningController {
},
})
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);
}
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')
@ -159,11 +196,17 @@ export class BatchMiningController {
@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);
}
@ -173,31 +216,39 @@ export class BatchMiningController {
'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 actualSheet = workbook.SheetNames.includes('Sheet2')
? workbook.Sheets['Sheet2']
: workbook.Sheets[workbook.SheetNames[0]];
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,
@ -208,6 +259,8 @@ export class BatchMiningController {
admin.id,
);
this.logger.log(`[POST /batch-mining/upload-execute] 执行成功: successCount=${result.successCount}, totalAmount=${result.totalAmount}`);
return {
...result,
originalFileName: file.originalname,
@ -216,6 +269,7 @@ export class BatchMiningController {
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,
@ -252,34 +306,59 @@ export class BatchMiningController {
@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})`);
return this.batchMiningService.execute(
{
items: body.items,
operatorId: admin.id,
operatorName: admin.username,
reason: body.reason,
},
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() {
const execution = await this.batchMiningService.getExecution();
if (!execution) {
throw new HttpException('尚未执行过批量补发', HttpStatus.NOT_FOUND);
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;
}
return execution;
}
}

View File

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

View File

@ -112,7 +112,9 @@ export class BatchMiningService {
*
*/
async hasExecuted(): Promise<boolean> {
this.logger.log('[hasExecuted] 检查批量补发是否已执行...');
const record = await this.prisma.batchMiningExecution.findFirst();
this.logger.log(`[hasExecuted] 结果: ${!!record}`);
return !!record;
}
@ -120,6 +122,9 @@ export class BatchMiningService {
*
*/
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();
if (alreadyExecuted) {
@ -136,16 +141,20 @@ export class BatchMiningService {
}
// 获取挖矿配置
this.logger.log('[preview] 获取挖矿配置...');
const config = await this.miningConfigRepository.getConfig();
if (!config) {
this.logger.error('[preview] 挖矿配置不存在');
throw new BadRequestException('挖矿配置不存在');
}
const secondDistribution = config.secondDistribution.value;
this.logger.log(`[preview] 每秒分配量: ${secondDistribution.toString()}`);
// 按批次分组并排序
const batchGroups = this.groupByBatch(items);
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 grandTotalAmount = new Decimal(0);
@ -199,7 +208,7 @@ export class BatchMiningService {
grandTotalAmount = grandTotalAmount.plus(batchTotalAmount);
}
return {
const result = {
canExecute: true,
alreadyExecuted: false,
totalBatches: sortedBatches.length,
@ -208,6 +217,8 @@ export class BatchMiningService {
grandTotalAmount: 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> {
const { items, operatorId, operatorName, reason } = request;
this.logger.log(`[execute] 开始执行批量补发, 操作人: ${operatorName}, 共 ${items.length} 条数据`);
this.logger.log(`[execute] 原因: ${reason}`);
// 检查是否已执行过
const alreadyExecuted = await this.hasExecuted();
@ -446,6 +459,7 @@ export class BatchMiningService {
*
*/
async getExecution(): Promise<any | null> {
this.logger.log('[getExecution] 获取批量补发执行记录...');
const execution = await this.prisma.batchMiningExecution.findFirst({
include: {
records: {
@ -455,9 +469,11 @@ export class BatchMiningService {
});
if (!execution) {
this.logger.log('[getExecution] 未找到执行记录');
return null;
}
this.logger.log(`[getExecution] 找到执行记录: id=${execution.id}, records=${execution.records.length}`);
return {
id: execution.id,
operatorId: execution.operatorId,

View File

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