This commit is contained in:
杨志
2026-01-21 09:41:22 +08:00
parent eed3540f3d
commit 08cb6b2b03
2 changed files with 197 additions and 56 deletions

View File

@@ -275,6 +275,72 @@ class CrawlerService
return $cookies; 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 原始职位数据 * @param array $item 原始职位数据
@@ -288,10 +354,12 @@ class CrawlerService
$item = $item[0]; $item = $item[0];
} }
// 计算竞争比(格式:招聘人数:审核通过人数 // 获取招聘人数审核通过人数
$zprs = isset($item['zprs']) ? intval($item['zprs']) : 0; $zprs = isset($item['zprs']) ? intval($item['zprs']) : 0;
$bkrs = isset($item['bkrs']) ? intval($item['bkrs']) : 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 [ return [
'sbmc' => $item['sbmc'] ?? '', // 省份 'sbmc' => $item['sbmc'] ?? '', // 省份
@@ -301,7 +369,7 @@ class CrawlerService
'zwdm' => $item['zwdm'] ?? $zwdm, // 职位代码 'zwdm' => $item['zwdm'] ?? $zwdm, // 职位代码
'zprs' => $zprs, // 招聘人数 'zprs' => $zprs, // 招聘人数
'bkrs' => $bkrs, // 审核通过人数 'bkrs' => $bkrs, // 审核通过人数
'competition_ratio' => $competitionRatio, // 竞争比(格式:招聘人数:审核通过人数) 'competition_ratio' => $competitionRatio, // 竞争比(格式:1:比例保留2位小数)
]; ];
} }

View File

@@ -233,6 +233,34 @@
.select-all { .select-all {
margin-bottom: 10px; 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;
}
</style> </style>
</head> </head>
<body> <body>
@@ -291,10 +319,16 @@
<h2>第二步:选择地区并自动抓取</h2> <h2>第二步:选择地区并自动抓取</h2>
<div id="dsdm-message"></div> <div id="dsdm-message"></div>
<div class="form-group"> <div class="form-group">
<label for="dsdm">地区代码dsdm</label> <label>地区代码dsdm</label>
<select id="dsdm"> <div class="select-all">
<option value="">请先获取地区选项</option> <label style="display: inline-flex; align-items: center; cursor: pointer;">
</select> <input type="checkbox" id="select-all-dsdm" onchange="toggleAllDsdm()" style="width: auto; margin-right: 8px;">
<span>全选</span>
</label>
</div>
<div id="dsdm-list" style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; padding: 10px;">
<div style="color: #999; text-align: center; padding: 20px;">请先获取地区选项</div>
</div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button class="btn" onclick="fetchAllPositions()">自动抓取全部职位</button> <button class="btn" onclick="fetchAllPositions()">自动抓取全部职位</button>
@@ -430,17 +464,20 @@
}) })
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.code === 1) { if (data.code === 1 && Array.isArray(data.data)) {
const select = document.getElementById('dsdm'); const dsdmList = document.getElementById('dsdm-list');
select.innerHTML = '<option value="">请选择</option>'; dsdmList.innerHTML = '';
data.data.forEach(option => { data.data.forEach(option => {
const opt = document.createElement('option'); const div = document.createElement('div');
opt.value = option.value; div.className = 'dsdm-checkbox-item';
opt.textContent = option.text; div.innerHTML = `
select.appendChild(opt); <label>
<input type="checkbox" class="dsdm-checkbox" value="${option.value}" onchange="updateSelectAllDsdm()">
<span>${option.label || option.text} (${option.value})</span>
</label>
`;
dsdmList.appendChild(div);
}); });
showMessage('dsdm-message', '获取地区选项成功,请选择地区', 'success'); showMessage('dsdm-message', '获取地区选项成功,请选择地区', 'success');
} else { } else {
showMessage('dsdm-message', data.msg || '获取失败', 'error'); 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() { function getZwdmList() {
const dsdm = document.getElementById('dsdm').value; const dsdm = document.getElementById('dsdm').value;
@@ -633,7 +693,7 @@
<td>${item.zwdm || ''}</td> <td>${item.zwdm || ''}</td>
<td>${item.zprs || 0}</td> <td>${item.zprs || 0}</td>
<td>${item.bkrs || 0}</td> <td>${item.bkrs || 0}</td>
<td>${item.competition_ratio || '0.00'}</td> <td>${item.competition_ratio || '0:0'}</td>
`; `;
tbody.appendChild(tr); tbody.appendChild(tr);
document.getElementById('result-table').style.display = 'block'; document.getElementById('result-table').style.display = 'block';
@@ -650,15 +710,15 @@
// 自动抓取全部职位代码并逐条获取详情(流式展示,避免超时) // 自动抓取全部职位代码并逐条获取详情(流式展示,避免超时)
async function fetchAllPositions() { async function fetchAllPositions() {
const dsdm = document.getElementById('dsdm').value; const selectedDsdm = getSelectedDsdm();
const examid = document.getElementById('examid').value.trim(); const examid = document.getElementById('examid').value.trim();
const bmid = document.getElementById('bmid').value.trim(); const bmid = document.getElementById('bmid').value.trim();
const userid = document.getElementById('userid').value.trim(); const userid = document.getElementById('userid').value.trim();
const cookieData = buildCookiesPayload('result-message'); const cookieData = buildCookiesPayload('result-message');
const aa = lastAa; const aa = lastAa;
if (!dsdm) { if (!selectedDsdm || selectedDsdm.length === 0) {
showMessage('dsdm-message', '请先选择地区', 'error'); showMessage('dsdm-message', '请先选择至少一个地区', 'error');
return; return;
} }
if (!examid || !bmid || !userid) { if (!examid || !bmid || !userid) {
@@ -666,7 +726,7 @@
return; return;
} }
if (!aa) { if (!aa) {
showMessage('result-message', '请先点击获取地区选项生成aa', 'error'); showMessage('result-message', '请先点击"获取地区选项"生成aa', 'error');
return; return;
} }
if (!cookieData) { if (!cookieData) {
@@ -683,50 +743,63 @@
const tbody = document.getElementById('data-table-body'); const tbody = document.getElementById('data-table-body');
tbody.innerHTML = ''; tbody.innerHTML = '';
document.getElementById('result-table').style.display = 'block'; document.getElementById('result-table').style.display = 'block';
showMessage('result-message', '正在获取职位代码列表...', 'info');
// 获取全部职位代码 let totalCodes = 0;
const listResp = await fetch('/crawler/getZwdmList', { let processedCodes = 0;
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'); for (let dsdmIndex = 0; dsdmIndex < selectedDsdm.length; dsdmIndex++) {
// 爬取失败,恢复按钮状态 const dsdm = selectedDsdm[dsdmIndex];
isCrawling = false; showMessage('result-message', `正在处理地区 ${dsdm} (${dsdmIndex + 1}/${selectedDsdm.length})...`, 'info');
document.getElementById('export-btn').disabled = false;
document.getElementById('export-btn').textContent = '导出CSV';
return;
}
// 过滤掉152开头的职位代码 // 获取该地区的全部职位代码
const codes = listResp.data const listResp = await fetch(API_BASE_URL + '/crawler/getZwdmList', {
.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', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 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 })); }).then(r => r.json()).catch(e => ({ code: 0, msg: e.message }));
if (infoResp.code === 1 && infoResp.data) { if (listResp.code !== 1 || !Array.isArray(listResp.data) || listResp.data.length === 0) {
const item = infoResp.data; showMessage('result-message', `地区 ${dsdm} 获取职位代码失败: ${listResp.msg || '未获取到职位代码'}`, 'error');
lastResults.push(item); continue;
appendResultRow(item);
} else {
appendErrorRow(code, infoResp.msg || '获取失败');
} }
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'); showMessage('result-message', `完成,共 ${lastResults.length} 条成功,失败 ${codes.length - lastResults.length}`, 'success');
// 爬取完成,启用导出按钮 // 爬取完成,启用导出按钮