B端后台列表页自动新增素材方案
我设计了一套完整的浏览器自动化方案,使用 Puppeteer 实现B端后台列表页的自动新增素材功能。该方案包含数据组织、浏览器操作、错误处理等完整流程。
一、技术选型
- 浏览器自动化工具:Puppeteer (https://pptr.dev)
- 任务调度:Bull (https://github.com/OptimalBits/bull)
- 配置管理:dotenv (https://github.com/motdotla/dotenv)
- 日志记录:Winston (https://github.com/winstonjs/winston)
二、数据模型设计
首先定义素材数据结构,支持不同类型的素材:
// material.js
class Material {constructor(templateType, category, materialType, content) {this.templateType = templateType; // 模版类型:九宫格模版/幸运盲盒模版this.category = category; // 素材类别:背景图片/抽奖动画/立即下载文案this.materialType = materialType; // 素材类型:图片/文本/代码块this.content = content; // 素材内容:图片路径/文本内容/代码块内容this.status = 'pending'; // 处理状态this.createTime = new Date();}
}// 工厂方法创建不同类型的素材
class MaterialFactory {static createImageMaterial(templateType, category, filePath) {return new Material(templateType, category, '图片', { filePath });}static createTextMaterial(templateType, category, text) {return new Material(templateType, category, '文本', { text });}static createCodeMaterial(templateType, category, code) {return new Material(templateType, category, '代码块', { code });}
}module.exports = { Material, MaterialFactory };
三、浏览器自动化实现
使用 Puppeteer 实现浏览器自动操作流程:
// browser-automation.js
const puppeteer = require('puppeteer');
const { Material } = require('./material');
const logger = require('./logger');class BrowserAutomation {constructor(config) {this.config = config;this.browser = null;this.page = null;}// 初始化浏览器async init() {this.browser = await puppeteer.launch({headless: this.config.headless,args: ['--window-size=1920,1080'],slowMo: this.config.slowMo || 0});this.page = await this.browser.newPage();await this.page.setDefaultTimeout(this.config.timeout || 30000);}// 登录系统async login() {logger.info('开始登录系统');await this.page.goto(this.config.loginUrl);// 输入用户名和密码await this.page.type(this.config.selectors.username, this.config.username);await this.page.type(this.config.selectors.password, this.config.password);// 点击登录按钮await Promise.all([this.page.click(this.config.selectors.loginButton),this.page.waitForNavigation()]);logger.info('登录成功');}// 导航到素材列表页async navigateToListPage() {logger.info('导航到素材列表页');await this.page.goto(this.config.listPageUrl);await this.page.waitForSelector(this.config.selectors.listPage);}// 点击"新增AI素材"按钮async clickAddAIMaterialButton() {logger.info('点击"新增AI素材"按钮');await this.page.waitForSelector(this.config.selectors.addAIMaterialButton);await this.page.click(this.config.selectors.addAIMaterialButton);// 等待弹窗出现await this.page.waitForSelector(this.config.selectors.modal);}// 选择模版类型async selectTemplateType(templateType) {logger.info(`选择模版类型: ${templateType}`);await this.page.waitForSelector(this.config.selectors.templateTypeSelect);// 根据模版类型选择对应的选项值const templateValueMap = {'九宫格模版': 'nine-grid','幸运盲盒模版': 'lucky-box'};const templateValue = templateValueMap[templateType];if (!templateValue) {throw new Error(`未知的模版类型: ${templateType}`);}await this.page.select(this.config.selectors.templateTypeSelect, templateValue);}// 选择素材类别async selectCategory(category) {logger.info(`选择素材类别: ${category}`);await this.page.waitForSelector(this.config.selectors.categorySelect);// 根据素材类别选择对应的选项值const categoryValueMap = {'背景图片': 'background-image','抽奖动画': 'lottery-animation','立即下载文案': 'download-copywriting'};const categoryValue = categoryValueMap[category];if (!categoryValue) {throw new Error(`未知的素材类别: ${category}`);}await this.page.select(this.config.selectors.categorySelect, categoryValue);}// 选择素材类型async selectMaterialType(materialType) {logger.info(`选择素材类型: ${materialType}`);await this.page.waitForSelector(this.config.selectors.materialTypeSelect);// 根据素材类型选择对应的选项值const materialTypeValueMap = {'图片': 'image','文本': 'text','代码块': 'code'};const materialTypeValue = materialTypeValueMap[materialType];if (!materialTypeValue) {throw new Error(`未知的素材类型: ${materialType}`);}await this.page.select(this.config.selectors.materialTypeSelect, materialTypeValue);}// 处理素材内容async processContent(material) {logger.info(`处理素材内容: ${material.materialType}`);switch (material.materialType) {case '图片':await this.uploadImage(material.content.filePath);break;case '文本':await this.fillTextContent(material.content.text);break;case '代码块':await this.fillCodeContent(material.content.code);break;default:throw new Error(`不支持的素材类型: ${material.materialType}`);}}// 上传图片async uploadImage(filePath) {logger.info(`上传图片: ${filePath}`);await this.page.waitForSelector(this.config.selectors.imageUploadInput);const fileInput = await this.page.$(this.config.selectors.imageUploadInput);await fileInput.uploadFile(filePath);// 等待上传完成await this.page.waitForSelector(this.config.selectors.imageUploadSuccess, {timeout: 60000 // 图片上传可能需要较长时间});}// 填写文本内容async fillTextContent(text) {logger.info('填写文本内容');await this.page.waitForSelector(this.config.selectors.textContentArea);await this.page.type(this.config.selectors.textContentArea, text);}// 填写代码块内容async fillCodeContent(code) {logger.info('填写代码块内容');await this.page.waitForSelector(this.config.selectors.codeContentArea);await this.page.type(this.config.selectors.codeContentArea, code);}// 点击确定按钮完成入库async submitMaterial() {logger.info('点击确定按钮');await this.page.waitForSelector(this.config.selectors.confirmButton);// 点击确定并等待提交完成await Promise.all([this.page.click(this.config.selectors.confirmButton),this.page.waitForSelector(this.config.selectors.submitSuccessMessage, { timeout: 15000 })]);logger.info('素材入库成功');return true;}// 关闭浏览器async close() {if (this.browser) {await this.browser.close();}}// 执行完整的素材入库流程async processMaterial(material) {try {await this.init();await this.login();await this.navigateToListPage();await this.clickAddAIMaterialButton();await this.selectTemplateType(material.templateType);await this.selectCategory(material.category);await this.selectMaterialType(material.materialType);await this.processContent(material);const success = await this.submitMaterial();material.status = success ? 'success' : 'failed';return { success, material };} catch (error) {logger.error(`处理素材失败: ${error.message}`);material.status = 'failed';material.errorMessage = error.message;return { success: false, material, error };} finally {await this.close();}}
}module.exports = BrowserAutomation;
四、配置管理
创建配置文件管理选择器和环境变量:
// config.js
require('dotenv').config();module.exports = {// 环境配置headless: process.env.HEADLESS === 'true',slowMo: parseInt(process.env.SLOW_MO) || 0,timeout: parseInt(process.env.TIMEOUT) || 30000,// 登录配置loginUrl: process.env.LOGIN_URL,username: process.env.USERNAME,password: process.env.PASSWORD,// 页面URLlistPageUrl: process.env.LIST_PAGE_URL,// DOM选择器配置selectors: {// 登录页面username: '#username',password: '#password',loginButton: '#login-button',// 列表页面listPage: '.material-list',addAIMaterialButton: '#add-ai-material-btn',// 新增弹窗modal: '.add-material-modal',templateTypeSelect: '#template-type',categorySelect: '#category',materialTypeSelect: '#material-type',// 内容区域imageUploadInput: 'input[type="file"]',imageUploadSuccess: '.upload-success',textContentArea: '#text-content',codeContentArea: '#code-content',// 提交区域confirmButton: '.confirm-btn',submitSuccessMessage: '.success-message'}
};
五、任务调度与批量处理
使用 Bull 实现任务队列,支持批量处理多个素材:
// queue.js
const Queue = require('bull');
const BrowserAutomation = require('./browser-automation');
const config = require('./config');
const logger = require('./logger');// 创建任务队列
const materialQueue = new Queue('material-upload', {redis: {host: process.env.REDIS_HOST || 'localhost',port: parseInt(process.env.REDIS_PORT) || 6379}
});// 处理队列任务
materialQueue.process(async (job) => {const material = job.data;logger.info(`开始处理素材任务: ${material.id}`);const automation = new BrowserAutomation(config);return await automation.processMaterial(material);
});// 添加素材到队列
async function enqueueMaterial(material) {return await materialQueue.add(material, {attempts: 3, // 失败重试3次backoff: { type: 'exponential', delay: 1000 } // 重试间隔递增});
}// 监听队列事件
materialQueue.on('completed', (job, result) => {if (result.success) {logger.info(`素材任务 ${job.id} 处理成功`);} else {logger.error(`素材任务 ${job.id} 处理失败: ${result.error.message}`);}
});materialQueue.on('failed', (job, error) => {logger.error(`素材任务 ${job.id} 处理失败: ${error.message}`);
});module.exports = { enqueueMaterial };
六、使用示例
下面是如何使用上述代码的示例:
// app.js
const { MaterialFactory } = require('./material');
const { enqueueMaterial } = require('./queue');async function main() {// 创建不同类型的素材const imageMaterial = MaterialFactory.createImageMaterial('九宫格模版', '背景图片', './assets/background.jpg');const textMaterial = MaterialFactory.createTextMaterial('幸运盲盒模版', '立即下载文案', '点击下载,立即参与抽奖活动,赢取丰厚奖品!');const codeMaterial = MaterialFactory.createCodeMaterial('九宫格模版', '抽奖动画', 'function startLottery() { ... }');// 将素材添加到处理队列await enqueueMaterial(imageMaterial);await enqueueMaterial(textMaterial);await enqueueMaterial(codeMaterial);console.log('所有素材已添加到处理队列');
}main().catch(error => {console.error('执行过程中发生错误:', error);
});
七、部署与运行
-
安装依赖:
npm install puppeteer bull dotenv winston
-
创建
.env
文件配置环境变量:HEADLESS=true SLOW_MO=0 TIMEOUT=60000 LOGIN_URL=https://your-b-system.com/login USERNAME=your_username PASSWORD=your_password LIST_PAGE_URL=https://your-b-system.com/materials REDIS_HOST=localhost REDIS_PORT=6379
-
运行程序:
node app.js
八、关键技术点说明
-
Puppeteer 核心功能:
- 元素选择与操作:
page.$
、page.$$
、page.click
、page.type
- 页面导航与等待:
page.goto
、page.waitForSelector
、page.waitForNavigation
- 文件上传:
uploadFile
方法 - 无头模式:
headless
选项
- 元素选择与操作:
-
错误处理:
- 重试机制:通过 Bull 队列实现自动重试
- 超时设置:全局超时和特定操作超时
- 日志记录:使用 Winston 记录详细操作日志
-
配置化设计:
- 将选择器和环境变量外部化,便于适配不同系统
- 支持通过环境变量控制执行模式(无头/有头、执行速度)
通过这个方案,您可以实现B端后台列表页的自动化新增素材功能,支持多种模版类型、素材类别和素材类型,大幅提高工作效率。