up
This commit is contained in:
@@ -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位小数)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,49 +743,62 @@
|
|||||||
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;
|
||||||
|
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', {
|
const listResp = await fetch(API_BASE_URL + '/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', {
|
|
||||||
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');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user