修复竞争比和按钮问题

This commit is contained in:
杨志
2026-01-21 10:42:18 +08:00
parent 08cb6b2b03
commit bc622c6c34
6 changed files with 124 additions and 64 deletions

View File

@@ -1,11 +0,0 @@
APP_DEBUG = true
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = test
DB_USER = username
DB_PASS = password
DB_PORT = 3306
DB_CHARSET = utf8
DEFAULT_LANG = zh-cn

View File

@@ -292,7 +292,7 @@ class CrawlerService
}
/**
* 计算竞争比(格式:招聘人数:审核通过人数,简化比例保留2位小数
* 计算竞争比(格式:1:比例保留2位小数
* @param int $zprs 招聘人数
* @param int $bkrs 审核通过人数
* @return string
@@ -304,33 +304,13 @@ class CrawlerService
}
if ($bkrs <= 0) {
return $zprs . ':0';
return '1:0';
}
// 计算最大公约数
$gcd = $this->gcd($zprs, $bkrs);
// 将招聘人数简化为1计算审核通过人数与招聘人数的比例
$ratio = $bkrs / $zprs;
// 简化比例
$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;
// 保留2位小数
$ratioFormatted = number_format($ratio, 2, '.', '');
// 如果小数部分是.00,则显示为整数

View File

@@ -23,7 +23,8 @@
"php": ">=8.0.0",
"topthink/framework": "^8.0",
"topthink/think-orm": "^3.0|^4.0",
"topthink/think-filesystem": "^2.0|^3.0"
"topthink/think-filesystem": "^2.0|^3.0",
"topthink/think-view": "^2.0"
},
"require-dev": {
"topthink/think-dumper": "^1.0",

4
omposer Normal file
View File

@@ -0,0 +1,4 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

0
php Normal file
View File

View File

@@ -4,6 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>职位信息爬虫工具</title>
<!-- SheetJS库用于导出XLSX -->
<script src="/static/js/xlsx.full.min.js"></script>
<style>
* {
margin: 0;
@@ -331,7 +333,7 @@
</div>
</div>
<div class="action-buttons">
<button class="btn" onclick="fetchAllPositions()">自动抓取全部职位</button>
<button class="btn" id="fetch-all-btn" onclick="debouncedFetchAllPositions()">自动抓取全部职位</button>
</div>
</div>
@@ -339,7 +341,7 @@
<div class="form-section">
<h2>职位信息结果</h2>
<div class="action-buttons" style="margin-bottom: 15px;">
<button class="btn btn-secondary" id="export-btn" onclick="exportCsv()" disabled>导出CSV</button>
<button class="btn btn-secondary" id="export-btn" onclick="exportXlsx()" disabled>导出XLSX</button>
</div>
<div id="result-message"></div>
<div class="table-container" id="result-table" style="display: none;">
@@ -372,6 +374,24 @@
let lastResults = [];
let isCrawling = false; // 爬取状态标志
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 防抖包装的抓取函数300ms防抖
const debouncedFetchAllPositions = debounce(() => {
fetchAllPositions();
}, 300);
// 页面加载时自动加载用户配置
window.onload = function() {
loadUserConfig();
@@ -710,6 +730,12 @@
// 自动抓取全部职位代码并逐条获取详情(流式展示,避免超时)
async function fetchAllPositions() {
// 如果正在爬取,直接返回
if (isCrawling) {
showMessage('result-message', '正在爬取中,请勿重复点击', 'error');
return;
}
const selectedDsdm = getSelectedDsdm();
const examid = document.getElementById('examid').value.trim();
const bmid = document.getElementById('bmid').value.trim();
@@ -733,10 +759,19 @@
return;
}
// 开始爬取,禁用导出按钮
// 开始爬取,禁用所有相关按钮
isCrawling = true;
document.getElementById('export-btn').disabled = true;
document.getElementById('export-btn').textContent = '爬取中...';
const fetchBtnStart = document.getElementById('fetch-all-btn');
const exportBtnStart = document.getElementById('export-btn');
if (fetchBtnStart) {
fetchBtnStart.disabled = true;
fetchBtnStart.textContent = '爬取中...';
}
if (exportBtnStart) {
exportBtnStart.disabled = true;
exportBtnStart.textContent = '爬取中...';
}
// 清空旧数据
lastResults = [];
@@ -796,20 +831,22 @@
// 爬取完成,恢复按钮状态
isCrawling = false;
document.getElementById('export-btn').disabled = false;
document.getElementById('export-btn').textContent = '导出CSV';
const fetchBtnEnd = document.getElementById('fetch-all-btn');
const exportBtnEnd = document.getElementById('export-btn');
if (fetchBtnEnd) {
fetchBtnEnd.disabled = false;
fetchBtnEnd.textContent = '自动抓取全部职位';
}
if (exportBtnEnd) {
exportBtnEnd.disabled = false;
exportBtnEnd.textContent = '导出XLSX';
}
showMessage('result-message', `爬取完成!共处理 ${processedCodes} 个职位`, 'success');
showMessage('result-message', `完成,共 ${lastResults.length} 条成功,失败 ${codes.length - lastResults.length}`, 'success');
// 爬取完成,启用导出按钮
isCrawling = false;
document.getElementById('export-btn').disabled = false;
document.getElementById('export-btn').textContent = '导出CSV';
}
// 导出CSV
function exportCsv() {
// 导出XLSX
function exportXlsx() {
if (isCrawling) {
showMessage('result-message', '爬取进行中,请等待完成后再导出', 'error');
return;
@@ -818,8 +855,17 @@
showMessage('result-message', '暂无数据可导出', 'error');
return;
}
// 检查SheetJS库是否加载
if (typeof XLSX === 'undefined') {
showMessage('result-message', '导出库加载失败,请刷新页面重试', 'error');
return;
}
// 准备数据
const headers = ['省份','地区','招聘单位/用人司局','职位名称','职位代码','招聘人数','审核通过人数','竞争比'];
const lines = [headers.join(',')];
const data = [headers];
lastResults.forEach(item => {
const row = [
item.sbmc || '',
@@ -829,18 +875,58 @@
item.zwdm || '',
item.zprs || 0,
item.bkrs || 0,
item.competition_ratio || '0:0'
item.competition_ratio || '0:0' // 使用与网页显示一致的竞争比格式
];
lines.push(row.map(v => `"${String(v).replace(/"/g, '""')}"`).join(','));
data.push(row);
});
const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'positions.csv';
link.click();
URL.revokeObjectURL(url);
showMessage('result-message', '已导出CSV', 'success');
// 创建工作簿
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet(data);
// 设置列宽
const colWidths = [
{ wch: 10 }, // 省份
{ wch: 15 }, // 地区
{ wch: 25 }, // 招聘单位/用人司局
{ wch: 25 }, // 职位名称
{ wch: 15 }, // 职位代码
{ wch: 12 }, // 招聘人数
{ wch: 15 }, // 审核通过人数
{ wch: 12 } // 竞争比
];
ws['!cols'] = colWidths;
// 设置表头样式(如果需要)
const headerRange = XLSX.utils.decode_range(ws['!ref']);
for (let col = headerRange.s.c; col <= headerRange.e.c; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: 0, c: col });
if (!ws[cellAddress]) continue;
ws[cellAddress].s = {
font: { bold: true },
fill: { fgColor: { rgb: "E0E0E0" } },
alignment: { horizontal: "center", vertical: "center" }
};
}
// 竞争比列设置为文本格式,防止被当作数字或日期
const competitionColIndex = 7; // 第8列
for (let r = 1; r < data.length; r++) { // 从第2行开始跳过表头
const addr = XLSX.utils.encode_cell({ r, c: competitionColIndex });
if (ws[addr]) {
ws[addr].t = 's';
ws[addr].z = '@';
}
}
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, '职位信息');
// 导出文件
const fileName = 'positions_' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '.xlsx';
XLSX.writeFile(wb, fileName);
showMessage('result-message', '已导出XLSX', 'success');
}
// 显示消息