From 08cb6b2b0387b2277095ef5d8e0b75bda660ebb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=BF=97?= Date: Wed, 21 Jan 2026 09:41:22 +0800 Subject: [PATCH] up --- app/service/CrawlerService.php | 74 +++++++++++++- view/crawler/index.html | 179 +++++++++++++++++++++++---------- 2 files changed, 197 insertions(+), 56 deletions(-) diff --git a/app/service/CrawlerService.php b/app/service/CrawlerService.php index 681eaf5..4385628 100644 --- a/app/service/CrawlerService.php +++ b/app/service/CrawlerService.php @@ -275,6 +275,72 @@ class CrawlerService return $cookies; } + /** + * 计算最大公约数(GCD) + * @param int $a + * @param int $b + * @return int + */ + private function gcd(int $a, int $b): int + { + while ($b != 0) { + $temp = $b; + $b = $a % $b; + $a = $temp; + } + return $a; + } + + /** + * 计算竞争比(格式:招聘人数:审核通过人数,简化比例,保留2位小数) + * @param int $zprs 招聘人数 + * @param int $bkrs 审核通过人数 + * @return string + */ + private function calculateCompetitionRatio(int $zprs, int $bkrs): string + { + if ($zprs <= 0) { + return '0:0'; + } + + if ($bkrs <= 0) { + return $zprs . ':0'; + } + + // 计算最大公约数 + $gcd = $this->gcd($zprs, $bkrs); + + // 简化比例 + $simplifiedZprs = $zprs / $gcd; + $simplifiedBkrs = $bkrs / $gcd; + + // 如果两个数都是整数,直接返回 + if ($simplifiedZprs == intval($simplifiedZprs) && $simplifiedBkrs == intval($simplifiedBkrs)) { + return intval($simplifiedZprs) . ':' . intval($simplifiedBkrs); + } + + // 如果招聘人数是1,审核通过人数保留2位小数 + if ($simplifiedZprs == 1) { + $bkrsFormatted = number_format($simplifiedBkrs, 2, '.', ''); + // 如果小数部分是.00,则显示为整数 + if (floatval($bkrsFormatted) == intval($bkrsFormatted)) { + return '1:' . intval($bkrsFormatted); + } + return '1:' . $bkrsFormatted; + } + + // 否则,将招聘人数简化为1,审核通过人数按比例计算并保留2位小数 + $ratio = $simplifiedBkrs / $simplifiedZprs; + $ratioFormatted = number_format($ratio, 2, '.', ''); + + // 如果小数部分是.00,则显示为整数 + if (floatval($ratioFormatted) == intval($ratioFormatted)) { + return '1:' . intval($ratioFormatted); + } + + return '1:' . $ratioFormatted; + } + /** * 格式化职位信息(包含竞争比计算) * @param array $item 原始职位数据 @@ -288,10 +354,12 @@ class CrawlerService $item = $item[0]; } - // 计算竞争比(格式:招聘人数:审核通过人数) + // 获取招聘人数和审核通过人数 $zprs = isset($item['zprs']) ? intval($item['zprs']) : 0; $bkrs = isset($item['bkrs']) ? intval($item['bkrs']) : 0; - $competitionRatio = $zprs > 0 && $bkrs > 0 ? $zprs . ':' . $bkrs : ($zprs > 0 ? $zprs . ':0' : '0:0'); + + // 计算竞争比(格式:1:比例,保留2位小数) + $competitionRatio = $this->calculateCompetitionRatio($zprs, $bkrs); return [ 'sbmc' => $item['sbmc'] ?? '', // 省份 @@ -301,7 +369,7 @@ class CrawlerService 'zwdm' => $item['zwdm'] ?? $zwdm, // 职位代码 'zprs' => $zprs, // 招聘人数 'bkrs' => $bkrs, // 审核通过人数 - 'competition_ratio' => $competitionRatio, // 竞争比(格式:招聘人数:审核通过人数) + 'competition_ratio' => $competitionRatio, // 竞争比(格式:1:比例,保留2位小数) ]; } diff --git a/view/crawler/index.html b/view/crawler/index.html index e6fb1c1..046326c 100644 --- a/view/crawler/index.html +++ b/view/crawler/index.html @@ -233,6 +233,34 @@ .select-all { margin-bottom: 10px; } + + .dsdm-checkbox-item { + padding: 8px; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; + } + + .dsdm-checkbox-item:last-child { + border-bottom: none; + } + + .dsdm-checkbox-item label { + display: flex; + align-items: center; + cursor: pointer; + width: 100%; + margin: 0; + } + + .dsdm-checkbox-item input[type="checkbox"] { + width: auto; + margin-right: 8px; + } + + #dsdm-list { + background: #fff; + } @@ -291,10 +319,16 @@

第二步:选择地区并自动抓取

- - + +
+ +
+
+
请先获取地区选项
+
@@ -430,17 +464,20 @@ }) .then(response => response.json()) .then(data => { - if (data.code === 1) { - const select = document.getElementById('dsdm'); - select.innerHTML = ''; - + if (data.code === 1 && Array.isArray(data.data)) { + const dsdmList = document.getElementById('dsdm-list'); + dsdmList.innerHTML = ''; data.data.forEach(option => { - const opt = document.createElement('option'); - opt.value = option.value; - opt.textContent = option.text; - select.appendChild(opt); + const div = document.createElement('div'); + div.className = 'dsdm-checkbox-item'; + div.innerHTML = ` + + `; + dsdmList.appendChild(div); }); - showMessage('dsdm-message', '获取地区选项成功,请选择地区', 'success'); } else { showMessage('dsdm-message', data.msg || '获取失败', 'error'); @@ -451,6 +488,29 @@ }); } + // 全选/取消全选地区 + function toggleAllDsdm() { + const selectAll = document.getElementById('select-all-dsdm').checked; + const checkboxes = document.querySelectorAll('.dsdm-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.checked = selectAll; + }); + } + + // 更新全选复选框状态 + function updateSelectAllDsdm() { + const checkboxes = document.querySelectorAll('.dsdm-checkbox'); + const checkedCount = document.querySelectorAll('.dsdm-checkbox:checked').length; + const selectAllCheckbox = document.getElementById('select-all-dsdm'); + selectAllCheckbox.checked = checkboxes.length > 0 && checkedCount === checkboxes.length; + } + + // 获取选中的地区代码数组 + function getSelectedDsdm() { + const checkboxes = document.querySelectorAll('.dsdm-checkbox:checked'); + return Array.from(checkboxes).map(cb => cb.value); + } + // 获取职位代码列表 function getZwdmList() { const dsdm = document.getElementById('dsdm').value; @@ -633,7 +693,7 @@ ${item.zwdm || ''} ${item.zprs || 0} ${item.bkrs || 0} - ${item.competition_ratio || '0.00'} + ${item.competition_ratio || '0:0'} `; tbody.appendChild(tr); document.getElementById('result-table').style.display = 'block'; @@ -650,15 +710,15 @@ // 自动抓取全部职位代码并逐条获取详情(流式展示,避免超时) async function fetchAllPositions() { - const dsdm = document.getElementById('dsdm').value; + const selectedDsdm = getSelectedDsdm(); const examid = document.getElementById('examid').value.trim(); const bmid = document.getElementById('bmid').value.trim(); const userid = document.getElementById('userid').value.trim(); const cookieData = buildCookiesPayload('result-message'); const aa = lastAa; - if (!dsdm) { - showMessage('dsdm-message', '请先选择地区', 'error'); + if (!selectedDsdm || selectedDsdm.length === 0) { + showMessage('dsdm-message', '请先选择至少一个地区', 'error'); return; } if (!examid || !bmid || !userid) { @@ -666,7 +726,7 @@ return; } if (!aa) { - showMessage('result-message', '请先点击“获取地区选项”生成aa', 'error'); + showMessage('result-message', '请先点击"获取地区选项"生成aa', 'error'); return; } if (!cookieData) { @@ -683,49 +743,62 @@ const tbody = document.getElementById('data-table-body'); tbody.innerHTML = ''; document.getElementById('result-table').style.display = 'block'; - showMessage('result-message', '正在获取职位代码列表...', 'info'); + + let totalCodes = 0; + let processedCodes = 0; + + // 遍历所有选中的地区 + for (let dsdmIndex = 0; dsdmIndex < selectedDsdm.length; dsdmIndex++) { + const dsdm = selectedDsdm[dsdmIndex]; + showMessage('result-message', `正在处理地区 ${dsdm} (${dsdmIndex + 1}/${selectedDsdm.length})...`, 'info'); - // 获取全部职位代码 - const listResp = await fetch('/crawler/getZwdmList', { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: `dsdm=${encodeURIComponent(dsdm)}&examid=${encodeURIComponent(examid)}&bmid=${encodeURIComponent(bmid)}&userid=${encodeURIComponent(userid)}&aa=${encodeURIComponent(aa)}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}` - }).then(r => r.json()).catch(e => ({ code: 0, msg: e.message })); - - if (listResp.code !== 1 || !Array.isArray(listResp.data) || listResp.data.length === 0) { - showMessage('result-message', listResp.msg || '未获取到职位代码', 'error'); - // 爬取失败,恢复按钮状态 - isCrawling = false; - document.getElementById('export-btn').disabled = false; - document.getElementById('export-btn').textContent = '导出CSV'; - return; - } - - // 过滤掉152开头的职位代码 - const codes = listResp.data - .map(it => it.zwdm) - .filter(code => !code.startsWith('152')); - showMessage('result-message', `共 ${codes.length} 个职位(已跳过152开头),开始逐条获取...`, 'info'); - - // 逐条获取职位详情 - for (let i = 0; i < codes.length; i++) { - const code = codes[i]; - const infoResp = await fetch(API_BASE_URL + '/crawler/getPositionInfo', { + // 获取该地区的全部职位代码 + const listResp = await fetch(API_BASE_URL + '/crawler/getZwdmList', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: `zwdm=${encodeURIComponent(code)}&examid=${encodeURIComponent(examid)}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}` + body: `dsdm=${encodeURIComponent(dsdm)}&examid=${encodeURIComponent(examid)}&bmid=${encodeURIComponent(bmid)}&userid=${encodeURIComponent(userid)}&aa=${encodeURIComponent(aa)}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}` }).then(r => r.json()).catch(e => ({ code: 0, msg: e.message })); - if (infoResp.code === 1 && infoResp.data) { - const item = infoResp.data; - lastResults.push(item); - appendResultRow(item); - } else { - appendErrorRow(code, infoResp.msg || '获取失败'); + if (listResp.code !== 1 || !Array.isArray(listResp.data) || listResp.data.length === 0) { + showMessage('result-message', `地区 ${dsdm} 获取职位代码失败: ${listResp.msg || '未获取到职位代码'}`, 'error'); + continue; } - showMessage('result-message', `进度:${i + 1}/${codes.length}`, 'info'); + // 过滤掉152开头的职位代码 + const codes = listResp.data + .map(it => it.zwdm) + .filter(code => !code.startsWith('152')); + + totalCodes += codes.length; + showMessage('result-message', `地区 ${dsdm}: 共 ${codes.length} 个职位(已跳过152开头),开始逐条获取...`, 'info'); + + // 逐条获取职位详情 + for (let i = 0; i < codes.length; i++) { + const code = codes[i]; + const infoResp = await fetch(API_BASE_URL + '/crawler/getPositionInfo', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `zwdm=${encodeURIComponent(code)}&examid=${encodeURIComponent(examid)}&cookies=${encodeURIComponent(JSON.stringify(cookieData))}` + }).then(r => r.json()).catch(e => ({ code: 0, msg: e.message })); + + if (infoResp.code === 1 && infoResp.data) { + const item = infoResp.data; + lastResults.push(item); + appendResultRow(item); + } else { + appendErrorRow(code, infoResp.msg || '获取失败'); + } + + processedCodes++; + showMessage('result-message', `总进度:${processedCodes}/${totalCodes} (地区 ${dsdmIndex + 1}/${selectedDsdm.length})`, 'info'); + } } + + // 爬取完成,恢复按钮状态 + isCrawling = false; + document.getElementById('export-btn').disabled = false; + document.getElementById('export-btn').textContent = '导出CSV'; + showMessage('result-message', `爬取完成!共处理 ${processedCodes} 个职位`, 'success'); showMessage('result-message', `完成,共 ${lastResults.length} 条成功,失败 ${codes.length - lastResults.length} 条`, 'success');