Files
amz-pic-flow/lib/batch-processor.js

333 lines
9.0 KiB
JavaScript
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.

/**
* 批量处理器
* 支持5000+ SKU的批量图片生成
*/
const axios = require('axios');
const fs = require('fs');
const path = require('path');
/**
* 批量处理配置
*/
const DEFAULT_CONFIG = {
concurrency: 1, // 并发数建议保持1避免API限流
retryCount: 3, // 失败重试次数
retryDelay: 5000, // 重试延迟(毫秒)
imageDelay: 1500, // 图片间延迟(毫秒)
skuDelay: 3000, // SKU间延迟毫秒
saveProgress: true, // 是否保存进度
progressFile: 'batch-progress.json'
};
/**
* 批量处理器类
*/
class BatchProcessor {
constructor(serverUrl, config = {}) {
this.serverUrl = serverUrl || 'http://localhost:3000';
this.config = { ...DEFAULT_CONFIG, ...config };
this.progress = {
total: 0,
completed: 0,
failed: 0,
results: []
};
}
/**
* 处理单个SKU
* @param {object} sku - SKU数据
* @returns {Promise<object>} 处理结果
*/
async processSKU(sku) {
const startTime = Date.now();
try {
console.log(`Processing SKU: ${sku.sku_id}`);
const response = await axios.post(`${this.serverUrl}/api/generate-sku`, {
sku
}, {
timeout: 600000 // 10分钟超时12张图
});
const duration = (Date.now() - startTime) / 1000;
console.log(`SKU ${sku.sku_id} completed in ${duration.toFixed(1)}s`);
return {
sku_id: sku.sku_id,
status: 'success',
duration,
...response.data
};
} catch (error) {
const duration = (Date.now() - startTime) / 1000;
console.error(`SKU ${sku.sku_id} failed after ${duration.toFixed(1)}s:`, error.message);
return {
sku_id: sku.sku_id,
status: 'failed',
duration,
error: error.message
};
}
}
/**
* 带重试的处理单个SKU
* @param {object} sku - SKU数据
* @returns {Promise<object>} 处理结果
*/
async processSKUWithRetry(sku) {
let lastError;
for (let attempt = 1; attempt <= this.config.retryCount; attempt++) {
try {
const result = await this.processSKU(sku);
if (result.status === 'success') {
return result;
}
lastError = result.error;
if (attempt < this.config.retryCount) {
console.log(`Retrying SKU ${sku.sku_id} (attempt ${attempt + 1}/${this.config.retryCount})...`);
await this.delay(this.config.retryDelay);
}
} catch (error) {
lastError = error.message;
if (attempt < this.config.retryCount) {
console.log(`Retrying SKU ${sku.sku_id} (attempt ${attempt + 1}/${this.config.retryCount})...`);
await this.delay(this.config.retryDelay);
}
}
}
return {
sku_id: sku.sku_id,
status: 'failed',
error: lastError,
attempts: this.config.retryCount
};
}
/**
* 批量处理SKU列表
* @param {array} skus - SKU数据数组
* @param {function} onProgress - 进度回调
* @returns {Promise<object>} 批量处理结果
*/
async processBatch(skus, onProgress = null) {
this.progress = {
total: skus.length,
completed: 0,
failed: 0,
startTime: Date.now(),
results: []
};
console.log(`\n${'='.repeat(60)}`);
console.log(`Starting batch processing: ${skus.length} SKUs`);
console.log(`${'='.repeat(60)}\n`);
// 加载之前的进度(如果有)
const existingProgress = this.loadProgress();
const processedIds = new Set(existingProgress.map(r => r.sku_id));
// 过滤掉已处理的SKU
const pendingSkus = skus.filter(sku => !processedIds.has(sku.sku_id));
this.progress.results = existingProgress;
this.progress.completed = existingProgress.filter(r => r.status === 'success').length;
this.progress.failed = existingProgress.filter(r => r.status === 'failed').length;
if (existingProgress.length > 0) {
console.log(`Resuming from previous progress: ${existingProgress.length} already processed`);
}
// 串行处理避免API限流
for (let i = 0; i < pendingSkus.length; i++) {
const sku = pendingSkus[i];
const overallIndex = existingProgress.length + i + 1;
console.log(`\n[${overallIndex}/${skus.length}] Processing ${sku.sku_id}...`);
const result = await this.processSKUWithRetry(sku);
this.progress.results.push(result);
if (result.status === 'success') {
this.progress.completed++;
} else {
this.progress.failed++;
}
// 保存进度
if (this.config.saveProgress) {
this.saveProgress();
}
// 进度回调
if (onProgress) {
onProgress({
current: overallIndex,
total: skus.length,
completed: this.progress.completed,
failed: this.progress.failed,
lastResult: result
});
}
// SKU间延迟
if (i < pendingSkus.length - 1) {
await this.delay(this.config.skuDelay);
}
}
this.progress.endTime = Date.now();
this.progress.totalDuration = (this.progress.endTime - this.progress.startTime) / 1000;
console.log(`\n${'='.repeat(60)}`);
console.log(`Batch processing complete!`);
console.log(`Total: ${skus.length}, Success: ${this.progress.completed}, Failed: ${this.progress.failed}`);
console.log(`Duration: ${this.progress.totalDuration.toFixed(1)}s`);
console.log(`${'='.repeat(60)}\n`);
return {
total: skus.length,
completed: this.progress.completed,
failed: this.progress.failed,
duration: this.progress.totalDuration,
results: this.progress.results
};
}
/**
* 延迟函数
* @param {number} ms - 毫秒数
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 保存进度到文件
*/
saveProgress() {
try {
const progressPath = path.join(process.cwd(), this.config.progressFile);
fs.writeFileSync(progressPath, JSON.stringify(this.progress.results, null, 2));
} catch (error) {
console.error('Failed to save progress:', error.message);
}
}
/**
* 加载之前的进度
* @returns {array} 之前的处理结果
*/
loadProgress() {
try {
const progressPath = path.join(process.cwd(), this.config.progressFile);
if (fs.existsSync(progressPath)) {
const content = fs.readFileSync(progressPath, 'utf-8');
return JSON.parse(content);
}
} catch (error) {
console.error('Failed to load progress:', error.message);
}
return [];
}
/**
* 清除进度文件
*/
clearProgress() {
try {
const progressPath = path.join(process.cwd(), this.config.progressFile);
if (fs.existsSync(progressPath)) {
fs.unlinkSync(progressPath);
console.log('Progress cleared');
}
} catch (error) {
console.error('Failed to clear progress:', error.message);
}
}
/**
* 生成批处理报告
* @returns {object} 报告数据
*/
generateReport() {
const successResults = this.progress.results.filter(r => r.status === 'success');
const failedResults = this.progress.results.filter(r => r.status === 'failed');
const totalImages = successResults.reduce((sum, r) => {
return sum + (r.summary?.success || 0);
}, 0);
return {
summary: {
total_skus: this.progress.total,
successful_skus: this.progress.completed,
failed_skus: this.progress.failed,
success_rate: ((this.progress.completed / this.progress.total) * 100).toFixed(1) + '%',
total_images_generated: totalImages,
total_duration: this.progress.totalDuration?.toFixed(1) + 's',
average_time_per_sku: (this.progress.totalDuration / this.progress.total).toFixed(1) + 's'
},
failed_skus: failedResults.map(r => ({
sku_id: r.sku_id,
error: r.error
})),
successful_skus: successResults.map(r => ({
sku_id: r.sku_id,
images: r.summary
}))
};
}
}
/**
* 从文件加载SKU列表
* @param {string} filePath - 文件路径JSON或CSV
* @returns {array} SKU数组
*/
function loadSKUsFromFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath).toLowerCase();
if (ext === '.json') {
const data = JSON.parse(content);
return Array.isArray(data) ? data : [data];
}
// 简单CSV解析假设第一行是表头
if (ext === '.csv') {
const lines = content.split('\n').filter(line => line.trim());
const headers = lines[0].split(',').map(h => h.trim());
return lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim());
const obj = {};
headers.forEach((h, i) => {
obj[h] = values[i];
});
return obj;
});
}
throw new Error('Unsupported file format. Use .json or .csv');
}
module.exports = {
BatchProcessor,
loadSKUsFromFile,
DEFAULT_CONFIG
};