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');