28 KiB
28 KiB
客户端待开发功能文档
本文档说明客户端需要实现的功能,所有操作通过MQTT接收服务端指令执行
一、客户端架构概览
1.1 技术栈
- 自动化框架: Puppeteer / Playwright (推荐Playwright,支持更好的反爬)
- 运行环境: Node.js 18+
- 通信协议: MQTT (mqtt://192.144.167.231:1883)
- 消息格式: JSON
- 浏览器: Chromium (无头模式/有头模式可切换)
1.2 核心模块
client/
├── src/
│ ├── mqtt/ # MQTT通信模块
│ │ ├── client.js # MQTT客户端
│ │ └── handler.js # 消息处理器
│ ├── platforms/ # 平台适配器
│ │ ├── boss/ # Boss直聘
│ │ │ ├── login.js # 登录模块
│ │ │ ├── search.js # 搜索模块 ⚠️ 待开发
│ │ │ ├── apply.js # 投递模块 ⚠️ 待完善
│ │ │ ├── resume.js # 简历模块 ⚠️ 待开发
│ │ │ ├── chat.js # 聊天模块 ⚠️ 待完善
│ │ │ └── active.js # 保活模块 ⚠️ 待开发
│ │ └── zhilian/ # 智联招聘 (同上)
│ ├── utils/ # 工具函数
│ │ ├── browser.js # 浏览器管理
│ │ ├── antibot.js # 反爬虫处理
│ │ └── logger.js # 日志记录
│ └── index.js # 入口文件
└── config/
└── default.json # 配置文件
1.3 工作流程
服务端 → MQTT → 客户端接收 → 路由到平台模块 → 执行操作 → 返回结果 → MQTT → 服务端
二、高优先级功能 (P0)
与投递直接相关的核心功能
2.1 增强搜索功能 ⚠️ 待开发
功能说明
实现Boss直聘完整的搜索功能,支持所有筛选项和URL解析
MQTT指令
search_jobs_enhanced- 带完整筛选参数的搜索search_by_url- 直接解析URL搜索
目标URL示例
https://www.zhipin.com/web/geek/jobs?city=101020100&query=%E5%85%A8%E6%A0%88%E5%B7%A5%E7%A8%8B%E5%B8%88&experience=103°ree=203&salary=404
实现要点
1. URL参数解析
// platforms/boss/search.js
/**
* 解析Boss直聘搜索URL
* @param {string} url - 搜索URL
* @returns {Object} 解析后的参数对象
*/
function parseSearchUrl(url) {
const urlObj = new URL(url);
const params = {};
// 城市代码
if (urlObj.searchParams.get('city')) {
params.city = urlObj.searchParams.get('city');
}
// 搜索关键词
if (urlObj.searchParams.get('query')) {
params.query = decodeURIComponent(urlObj.searchParams.get('query'));
}
// 工作经验
if (urlObj.searchParams.get('experience')) {
params.experience = urlObj.searchParams.get('experience');
}
// 学历要求
if (urlObj.searchParams.get('degree')) {
params.degree = urlObj.searchParams.get('degree');
}
// 薪资范围
if (urlObj.searchParams.get('salary')) {
params.salary = urlObj.searchParams.get('salary');
}
// 公司规模
if (urlObj.searchParams.get('scale')) {
params.scale = urlObj.searchParams.get('scale');
}
// 融资阶段
if (urlObj.searchParams.get('stage')) {
params.stage = urlObj.searchParams.get('stage');
}
// 职位类型
if (urlObj.searchParams.get('position')) {
params.position = urlObj.searchParams.get('position');
}
// 行业领域
if (urlObj.searchParams.get('industry')) {
params.industry = urlObj.searchParams.get('industry');
}
return params;
}
/**
* 构建搜索URL
* @param {Object} params - 搜索参数
* @returns {string} 完整的搜索URL
*/
function buildSearchUrl(params) {
const baseUrl = 'https://www.zhipin.com/web/geek/jobs';
const searchParams = new URLSearchParams();
// 必填参数
if (params.city) searchParams.set('city', params.city);
if (params.query) searchParams.set('query', params.query);
// 可选筛选参数
if (params.experience) searchParams.set('experience', params.experience);
if (params.degree) searchParams.set('degree', params.degree);
if (params.salary) searchParams.set('salary', params.salary);
if (params.scale) searchParams.set('scale', params.scale);
if (params.stage) searchParams.set('stage', params.stage);
if (params.position) searchParams.set('position', params.position);
if (params.industry) searchParams.set('industry', params.industry);
return `${baseUrl}?${searchParams.toString()}`;
}
2. 搜索执行
/**
* 执行增强搜索
* @param {Page} page - Playwright页面对象
* @param {Object} params - 搜索参数
* @returns {Promise<Array>} 职位列表
*/
async function searchJobsEnhanced(page, params) {
const url = buildSearchUrl(params);
// 访问搜索页面
await page.goto(url, { waitUntil: 'networkidle' });
// 等待职位列表加载
await page.waitForSelector('.job-card-wrapper', { timeout: 10000 });
// 反爬虫检测
const isBlocked = await checkBotDetection(page);
if (isBlocked) {
throw new Error('检测到反爬虫,需要人工介入');
}
const jobs = [];
let hasMore = true;
let scrollCount = 0;
const maxScrolls = params.maxResults ? Math.ceil(params.maxResults / 30) : 3;
while (hasMore && scrollCount < maxScrolls) {
// 提取当前页面职位
const pageJobs = await extractJobsFromPage(page);
jobs.push(...pageJobs);
// 滚动加载更多
hasMore = await scrollToLoadMore(page);
scrollCount++;
// 随机延迟,模拟人类行为
await randomDelay(1000, 3000);
}
return jobs;
}
/**
* 从页面提取职位信息
* @param {Page} page - Playwright页面对象
* @returns {Promise<Array>} 职位信息数组
*/
async function extractJobsFromPage(page) {
return await page.$$eval('.job-card-wrapper', (cards) => {
return cards.map(card => {
const jobName = card.querySelector('.job-name')?.textContent.trim();
const jobArea = card.querySelector('.job-area')?.textContent.trim();
const salary = card.querySelector('.salary')?.textContent.trim();
const companyName = card.querySelector('.company-name a')?.textContent.trim();
const companyTag = Array.from(card.querySelectorAll('.company-tag-list li'))
.map(li => li.textContent.trim());
const bossInfo = card.querySelector('.info-public')?.textContent.trim();
const jobLink = card.querySelector('.job-card-left')?.href;
const jobId = jobLink?.match(/job_detail\/(\w+)\.html/)?.[1];
// 提取标签(经验、学历)
const tags = Array.from(card.querySelectorAll('.tag-list li'))
.map(li => li.textContent.trim());
return {
jobId,
jobName,
jobArea,
salary,
companyName,
companyTags: companyTag,
experience: tags[0] || '',
degree: tags[1] || '',
bossInfo,
jobLink,
tags
};
});
});
}
/**
* 滚动加载更多职位
* @param {Page} page - Playwright页面对象
* @returns {Promise<boolean>} 是否还有更多内容
*/
async function scrollToLoadMore(page) {
// 滚动到页面底部
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
// 等待新内容加载
await page.waitForTimeout(2000);
// 检查是否有"加载更多"按钮或已到底部
const loadMoreBtn = await page.$('.load-more');
if (loadMoreBtn) {
await loadMoreBtn.click();
await page.waitForTimeout(1500);
return true;
}
// 检查是否显示"没有更多了"
const noMore = await page.$('.no-more-tips');
return !noMore;
}
3. 反爬虫处理
// utils/antibot.js
/**
* 检测是否被反爬虫拦截
* @param {Page} page - Playwright页面对象
* @returns {Promise<boolean>} 是否被拦截
*/
async function checkBotDetection(page) {
const indicators = [
() => page.$('.captcha-container'), // 验证码
() => page.$('.security-check'), // 安全检查
() => page.content().then(c => c.includes('频繁访问')),
() => page.content().then(c => c.includes('请稍后再试'))
];
for (const check of indicators) {
if (await check()) {
return true;
}
}
return false;
}
/**
* 随机延迟
* @param {number} min - 最小毫秒
* @param {number} max - 最大毫秒
*/
async function randomDelay(min, max) {
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
await new Promise(resolve => setTimeout(resolve, delay));
}
/**
* 模拟人类鼠标移动
* @param {Page} page - Playwright页面对象
*/
async function simulateHumanBehavior(page) {
// 随机移动鼠标
const randomX = Math.floor(Math.random() * 800) + 100;
const randomY = Math.floor(Math.random() * 600) + 100;
await page.mouse.move(randomX, randomY);
// 随机滚动
await page.evaluate(() => {
const scrollAmount = Math.floor(Math.random() * 300) + 100;
window.scrollBy(0, scrollAmount);
});
await randomDelay(500, 1500);
}
4. MQTT指令处理器
// mqtt/handler.js - 增强搜索指令处理
async function handleSearchJobsEnhanced(payload) {
const { platform, params, taskId } = payload;
try {
// 获取平台对应的浏览器页面
const page = await browserManager.getPage(platform);
// 确保已登录
await ensureLoggedIn(page, platform);
// 执行搜索
const searchModule = require(`../platforms/${platform}/search.js`);
const jobs = await searchModule.searchJobsEnhanced(page, params);
// 返回结果
return {
code: 0,
message: '搜索成功',
data: {
total: jobs.length,
jobs: jobs
},
taskId
};
} catch (error) {
console.error('搜索失败:', error);
return {
code: -1,
message: error.message,
taskId
};
}
}
async function handleSearchByUrl(payload) {
const { platform, url, taskId } = payload;
try {
const searchModule = require(`../platforms/${platform}/search.js`);
// 解析URL
const params = searchModule.parseSearchUrl(url);
// 执行搜索
const page = await browserManager.getPage(platform);
await ensureLoggedIn(page, platform);
const jobs = await searchModule.searchJobsEnhanced(page, params);
return {
code: 0,
message: '搜索成功',
data: {
total: jobs.length,
jobs: jobs,
params: params // 返回解析的参数
},
taskId
};
} catch (error) {
console.error('URL搜索失败:', error);
return {
code: -1,
message: error.message,
taskId
};
}
}
参数映射表
工作经验 (experience)
103: 应届生104: 1年以内105: 1-3年106: 3-5年107: 5-10年108: 10年以上
学历要求 (degree)
203: 大专204: 本科205: 硕士206: 博士
薪资范围 (salary)
402: 3K以下403: 3-5K404: 5-10K405: 10-15K406: 15-25K407: 25-35K408: 35-50K409: 50K以上
公司规模 (scale)
301: 0-20人302: 20-99人303: 100-499人304: 500-999人305: 1000-9999人306: 10000人以上
融资阶段 (stage)
801: 未融资802: 天使轮803: A轮804: B轮805: C轮806: D轮及以上807: 已上市808: 不需要融资
测试用例
// 测试用例1: 参数搜索
{
action: 'search_jobs_enhanced',
platform: 'boss',
params: {
city: '101020100', // 上海
query: '全栈工程师',
experience: '106', // 3-5年
degree: '204', // 本科
salary: '406', // 15-25K
scale: '303', // 100-499人
maxResults: 100
}
}
// 测试用例2: URL搜索
{
action: 'search_by_url',
platform: 'boss',
url: 'https://www.zhipin.com/web/geek/jobs?city=101020100&query=%E5%85%A8%E6%A0%88%E5%B7%A5%E7%A8%8B%E5%B8%88&experience=106°ree=204&salary=406'
}
2.2 批量投递功能 ⚠️ 待完善
功能说明
支持批量投递职位,带智能过滤和频率控制
MQTT指令
batch_apply_jobs- 批量投递
实现要点
// platforms/boss/apply.js
/**
* 批量投递职位
* @param {Page} page - Playwright页面对象
* @param {Array} jobIds - 职位ID列表
* @param {Object} options - 投递选项
* @returns {Promise<Object>} 投递结果
*/
async function batchApplyJobs(page, jobIds, options = {}) {
const {
maxPerHour = 20, // 每小时最多投递数
delayMin = 10000, // 最小间隔(ms)
delayMax = 30000, // 最大间隔(ms)
skipApplied = true, // 跳过已投递
greetingMessage = '' // 打招呼消息
} = options;
const results = {
total: jobIds.length,
success: 0,
failed: 0,
skipped: 0,
details: []
};
let appliedThisHour = 0;
let hourStartTime = Date.now();
for (const jobId of jobIds) {
// 频率控制
if (appliedThisHour >= maxPerHour) {
const elapsed = Date.now() - hourStartTime;
if (elapsed < 3600000) {
const waitTime = 3600000 - elapsed;
console.log(`已达每小时投递上限,等待 ${waitTime/1000} 秒`);
await new Promise(resolve => setTimeout(resolve, waitTime));
appliedThisHour = 0;
hourStartTime = Date.now();
}
}
try {
// 检查是否已投递
if (skipApplied) {
const applied = await checkIfApplied(page, jobId);
if (applied) {
results.skipped++;
results.details.push({ jobId, status: 'skipped', reason: '已投递' });
continue;
}
}
// 投递职位
await applyJob(page, jobId, greetingMessage);
results.success++;
appliedThisHour++;
results.details.push({ jobId, status: 'success' });
// 随机延迟
const delay = Math.floor(Math.random() * (delayMax - delayMin + 1)) + delayMin;
await new Promise(resolve => setTimeout(resolve, delay));
} catch (error) {
results.failed++;
results.details.push({
jobId,
status: 'failed',
reason: error.message
});
}
}
return results;
}
/**
* 检查职位是否已投递
*/
async function checkIfApplied(page, jobId) {
await page.goto(`https://www.zhipin.com/job_detail/${jobId}.html`);
await page.waitForTimeout(1000);
// 查找"继续沟通"或"已沟通"按钮
const appliedBtn = await page.$('.btn-applied, .btn-chatted');
return !!appliedBtn;
}
/**
* 投递单个职位
*/
async function applyJob(page, jobId, greetingMessage) {
await page.goto(`https://www.zhipin.com/job_detail/${jobId}.html`);
await page.waitForSelector('.btn-startchat', { timeout: 5000 });
// 点击"立即沟通"
await page.click('.btn-startchat');
// 如果有打招呼消息
if (greetingMessage) {
await page.waitForSelector('.chat-input', { timeout: 3000 });
await page.fill('.chat-input', greetingMessage);
await page.click('.send-btn');
}
await page.waitForTimeout(1000);
}
2.3 简历刷新功能 ⚠️ 待开发
功能说明
定时刷新简历,提升曝光度
MQTT指令
refresh_resume- 刷新简历
实现要点
// platforms/boss/resume.js
/**
* 刷新简历
* @param {Page} page - Playwright页面对象
* @returns {Promise<Object>} 刷新结果
*/
async function refreshResume(page) {
try {
// 进入简历管理页面
await page.goto('https://www.zhipin.com/web/geek/profile', {
waitUntil: 'networkidle'
});
// 查找刷新按钮
const refreshBtn = await page.$('.refresh-resume-btn, .update-resume-btn');
if (!refreshBtn) {
throw new Error('未找到刷新按钮');
}
// 检查是否可刷新(可能有时间限制)
const disabled = await refreshBtn.getAttribute('disabled');
if (disabled) {
// 获取下次可刷新时间
const nextRefreshTime = await page.$eval('.next-refresh-time', el => el.textContent);
throw new Error(`暂时无法刷新,下次可刷新时间: ${nextRefreshTime}`);
}
// 点击刷新
await refreshBtn.click();
await page.waitForTimeout(2000);
// 确认刷新
const confirmBtn = await page.$('.confirm-refresh');
if (confirmBtn) {
await confirmBtn.click();
await page.waitForTimeout(1000);
}
return {
success: true,
message: '简历刷新成功',
timestamp: new Date().toISOString()
};
} catch (error) {
throw new Error(`简历刷新失败: ${error.message}`);
}
}
/**
* 获取简历刷新状态
*/
async function getResumeRefreshStatus(page) {
await page.goto('https://www.zhipin.com/web/geek/profile');
const status = await page.evaluate(() => {
const refreshBtn = document.querySelector('.refresh-resume-btn');
const isDisabled = refreshBtn?.hasAttribute('disabled');
const nextTime = document.querySelector('.next-refresh-time')?.textContent;
const lastRefreshTime = document.querySelector('.last-refresh-time')?.textContent;
return {
canRefresh: !isDisabled,
nextRefreshTime: nextTime,
lastRefreshTime: lastRefreshTime
};
});
return status;
}
三、中优先级功能 (P1)
3.1 账号保活功能 ⚠️ 待开发
功能说明
模拟真实用户行为,防止账号被标记为机器人
MQTT指令
auto_active- 执行保活操作
实现要点
// platforms/boss/active.js
/**
* 执行保活操作
* @param {Page} page - Playwright页面对象
* @param {Object} options - 保活选项
*/
async function performActiveActions(page, options = {}) {
const {
duration = 300000, // 持续时间(ms) 默认5分钟
actions = ['browse', 'search', 'view_company', 'scroll']
} = options;
const startTime = Date.now();
const actionsPerformed = [];
while (Date.now() - startTime < duration) {
// 随机选择一个行为
const action = actions[Math.floor(Math.random() * actions.length)];
try {
switch (action) {
case 'browse':
await browseJobList(page);
break;
case 'search':
await performRandomSearch(page);
break;
case 'view_company':
await viewRandomCompany(page);
break;
case 'scroll':
await randomScroll(page);
break;
}
actionsPerformed.push({
action,
timestamp: new Date().toISOString()
});
// 随机延迟
await randomDelay(5000, 15000);
} catch (error) {
console.error(`保活操作失败: ${action}`, error);
}
}
return {
success: true,
duration: Date.now() - startTime,
actionsPerformed: actionsPerformed.length,
details: actionsPerformed
};
}
/**
* 浏览职位列表
*/
async function browseJobList(page) {
await page.goto('https://www.zhipin.com/web/geek/jobs', {
waitUntil: 'networkidle'
});
// 随机滚动
for (let i = 0; i < 3; i++) {
await page.evaluate(() => {
window.scrollBy(0, Math.random() * 500 + 200);
});
await randomDelay(1000, 3000);
}
}
/**
* 执行随机搜索
*/
async function performRandomSearch(page) {
const keywords = ['前端', '后端', 'Java', 'Python', '产品经理', '运营'];
const keyword = keywords[Math.floor(Math.random() * keywords.length)];
await page.goto(`https://www.zhipin.com/web/geek/jobs?query=${encodeURIComponent(keyword)}`);
await page.waitForTimeout(2000);
// 随机点击一个职位查看
const jobCards = await page.$$('.job-card-wrapper');
if (jobCards.length > 0) {
const randomIndex = Math.floor(Math.random() * Math.min(jobCards.length, 5));
await jobCards[randomIndex].click();
await page.waitForTimeout(3000);
}
}
/**
* 查看随机公司
*/
async function viewRandomCompany(page) {
await page.goto('https://www.zhipin.com/web/geek/jobs');
await page.waitForTimeout(2000);
const companyLinks = await page.$$('.company-name a');
if (companyLinks.length > 0) {
const randomIndex = Math.floor(Math.random() * Math.min(companyLinks.length, 5));
await companyLinks[randomIndex].click();
await page.waitForTimeout(5000);
}
}
/**
* 随机滚动
*/
async function randomScroll(page) {
const scrollTimes = Math.floor(Math.random() * 5) + 2;
for (let i = 0; i < scrollTimes; i++) {
await page.evaluate(() => {
const direction = Math.random() > 0.5 ? 1 : -1;
const distance = Math.random() * 300 + 100;
window.scrollBy(0, direction * distance);
});
await randomDelay(800, 2000);
}
}
3.2 智能聊天功能 ⚠️ 待完善
功能说明
自动回复HR消息,提高响应率
MQTT指令
send_greeting- 发送打招呼消息auto_reply_chat- 自动回复聊天
实现要点
// platforms/boss/chat.js
/**
* 发送打招呼消息
* @param {Page} page - Playwright页面对象
* @param {string} bossId - HRID
* @param {string} message - 消息内容
*/
async function sendGreeting(page, bossId, message) {
// 进入聊天页面
await page.goto(`https://www.zhipin.com/web/geek/chat?bosId=${bossId}`);
await page.waitForSelector('.chat-input', { timeout: 5000 });
// 输入消息
await page.fill('.chat-input', message);
await randomDelay(500, 1500);
// 发送
await page.click('.send-btn');
await page.waitForTimeout(1000);
return {
success: true,
bossId,
message,
timestamp: new Date().toISOString()
};
}
/**
* 自动回复聊天
* @param {Page} page - Playwright页面对象
* @param {Object} options - 回复选项
*/
async function autoReplyChat(page, options = {}) {
const {
checkInterval = 60000, // 检查间隔(ms)
replyDelay = 30000, // 回复延迟(ms) 模拟思考时间
maxReplies = 10 // 最多回复次数
} = options;
let repliedCount = 0;
// 获取未读消息
await page.goto('https://www.zhipin.com/web/geek/chat');
await page.waitForTimeout(2000);
const unreadChats = await page.$$('.chat-item.unread');
for (const chat of unreadChats) {
if (repliedCount >= maxReplies) break;
try {
// 点击进入聊天
await chat.click();
await page.waitForTimeout(1000);
// 获取最后一条消息
const lastMessage = await page.$eval('.message-list .message-item:last-child .message-text',
el => el.textContent.trim()
);
// 调用服务端AI生成回复
// 这里需要通过MQTT发送消息给服务端,服务端调用AI生成回复后返回
// 为了简化,这里暂时使用预设回复
const reply = generateAutoReply(lastMessage);
// 等待一段时间,模拟真人思考
await randomDelay(replyDelay * 0.8, replyDelay * 1.2);
// 发送回复
await page.fill('.chat-input', reply);
await page.click('.send-btn');
await page.waitForTimeout(1000);
repliedCount++;
} catch (error) {
console.error('自动回复失败:', error);
}
}
return {
success: true,
totalUnread: unreadChats.length,
replied: repliedCount
};
}
/**
* 生成自动回复(简化版,实际应调用服务端AI)
*/
function generateAutoReply(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes('面试') || lowerMessage.includes('interview')) {
return '好的,感谢您的面试邀请!请问具体的面试时间和地点是什么?';
}
if (lowerMessage.includes('简历') || lowerMessage.includes('resume')) {
return '您好,我已经投递了简历,请查收。如有任何问题,欢迎随时沟通!';
}
if (lowerMessage.includes('薪资') || lowerMessage.includes('salary')) {
return '关于薪资,我的期望是根据岗位要求和市场情况来定的,具体可以详细沟通。';
}
// 默认回复
return '收到,感谢您的消息!';
}
四、低优先级功能 (P2)
4.1 数据监控上报
/**
* 上报性能数据
*/
async function reportMetrics(metrics) {
const reportData = {
action: 'report_metrics',
metrics: {
...metrics,
timestamp: new Date().toISOString(),
deviceInfo: {
platform: process.platform,
memory: process.memoryUsage(),
uptime: process.uptime()
}
}
};
await mqttClient.publish(`device/${sn_code}/metrics`, JSON.stringify(reportData));
}
4.2 截图上报
/**
* 截图并上报
*/
async function captureAndReport(page, reason) {
const screenshot = await page.screenshot({ fullPage: false });
const base64 = screenshot.toString('base64');
await mqttClient.publish(`device/${sn_code}/screenshot`, JSON.stringify({
reason,
image: base64,
timestamp: new Date().toISOString()
}));
}
五、开发规范
5.1 错误处理
所有函数必须包含try-catch,并返回标准格式:
{
code: 0, // 0成功,-1失败
message: '操作成功',
data: {}, // 返回数据
taskId: 'xxx', // 任务ID
timestamp: 'ISO时间'
}
5.2 日志记录
const logger = require('../utils/logger');
logger.info('操作成功', { action: 'search_jobs', jobCount: 50 });
logger.error('操作失败', { action: 'apply_job', error: error.message });
logger.warn('检测到异常', { type: 'bot_detection' });
5.3 配置管理
// config/default.json
{
"mqtt": {
"broker": "mqtt://192.144.167.231:1883",
"username": "admin",
"password": "public"
},
"browser": {
"headless": false,
"slowMo": 50,
"defaultTimeout": 30000
},
"antibot": {
"randomDelay": {
"min": 1000,
"max": 3000
},
"humanBehavior": true
},
"limits": {
"maxApplyPerHour": 20,
"maxApplyPerDay": 100,
"maxSearchPerHour": 50
}
}
六、测试计划
6.1 单元测试
为每个模块编写单元测试:
// tests/platforms/boss/search.test.js
describe('Boss搜索模块', () => {
test('URL解析正确', () => {
const url = 'https://www.zhipin.com/web/geek/jobs?city=101020100&query=全栈';
const params = parseSearchUrl(url);
expect(params.city).toBe('101020100');
expect(params.query).toBe('全栈');
});
test('搜索执行成功', async () => {
const jobs = await searchJobsEnhanced(page, {
city: '101020100',
query: '前端工程师'
});
expect(jobs.length).toBeGreaterThan(0);
});
});
6.2 集成测试
测试完整的MQTT指令流程:
// tests/integration/search.test.js
describe('搜索功能集成测试', () => {
test('通过MQTT执行搜索', async () => {
const response = await mqttClient.publishAndWait('test_device', {
action: 'search_jobs_enhanced',
platform: 'boss',
params: {
city: '101020100',
query: '测试工程师'
}
});
expect(response.code).toBe(0);
expect(response.data.jobs).toBeDefined();
});
});
七、部署和运维
7.1 环境要求
- Node.js 18+
- Chromium/Chrome 浏览器
- 充足的内存(建议2GB+)
- 稳定的网络连接
7.2 启动命令
# 开发模式
npm run dev
# 生产模式
npm run start
# 后台运行(Linux)
pm2 start src/index.js --name "job-client"
7.3 监控指标
- 任务执行成功率
- 平均响应时间
- 反爬虫触发次数
- 账号状态(正常/异常)
八、开发时间估算
| 功能模块 | 优先级 | 预计工时 | 备注 |
|---|---|---|---|
| 增强搜索功能 | P0 | 3天 | 含URL解析和参数映射 |
| 批量投递功能 | P0 | 2天 | 含频率控制 |
| 简历刷新功能 | P0 | 1天 | 较简单 |
| 账号保活功能 | P1 | 2天 | 需模拟多种行为 |
| 智能聊天功能 | P1 | 3天 | 需AI对接 |
| 数据监控上报 | P2 | 1天 | 辅助功能 |
| 总计 | - | 12天 | 约2.5周 |
九、风险和注意事项
9.1 反爬虫风险
- 操作频率过高可能触发验证码
- 建议设置合理的延迟和频率限制
- 模拟真实用户行为(鼠标移动、随机滚动)
9.2 账号安全
- 避免同时操作过多账号
- 登录后保持Cookie有效期
- 定期执行保活操作
9.3 性能问题
- 浏览器实例占用较多内存
- 建议实现浏览器实例池管理
- 长时间运行需要定期重启
十、参考资料
文档版本: v1.0 最后更新: 2025-12-25 维护者: 开发团队