From f2c856ab05dad9daac5ab51326ff607b52a8b8f0 Mon Sep 17 00:00:00 2001
From: zhaojunlong <5482498@qq.com>
Date: Mon, 15 Dec 2025 21:06:10 +0800
Subject: [PATCH] =?UTF-8?q?```=20feat(scheduler):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E5=AE=9A=E6=97=B6=E4=BB=BB=E5=8A=A1=E9=85=8D=E7=BD=AE=E4=BB=A5?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=AD=E6=A0=87=E4=B8=8E=E6=8B=9B=E6=A0=87?=
=?UTF-8?q?=E5=88=86=E5=88=AB=E8=AE=BE=E7=BD=AE=E9=98=88=E5=80=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
将原先单一的 threshold 配置项拆分为 winningThreshold 和 bidThreshold,
分别用于控制中标公示和招标公告的金额筛选条件。同时调整了默认值及描述信息,
使配置更清晰灵活。
此外,更新了定时任务状态展示逻辑,支持显示两个独立的阈值及其单位转换(万元/亿元)。
当阈值为 0 时显示“不筛选”,提高用户理解度。
配置文件 config.json 中相关字段已同步修改,并调整了时间范围字段 timeRange 的默认值。
```
---
config.json | 9 +-
public/app.js | 331 ++++++++++++++++++++++++++-
public/index.html | 118 ++++++----
src/emailService.js | 535 +++++++++++++++++++++++++++++++++++++++++++-
src/scheduler.js | 308 +++++++++++++++++++++----
src/server.js | 407 +++++++++++++++++++++++++++++++--
6 files changed, 1573 insertions(+), 135 deletions(-)
diff --git a/config.json b/config.json
index 451c2d3..903e816 100644
--- a/config.json
+++ b/config.json
@@ -1,10 +1,11 @@
{
"scheduler": {
- "enabled": false,
+ "enabled": true,
"cronTime": "0 9 * * *",
- "threshold": 100000,
- "description": "每天9点采集当日大于10亿的项目",
- "timeRange": "today"
+ "winningThreshold": 0,
+ "bidThreshold": 0,
+ "description": "每天9点采集当日项目",
+ "timeRange": "thisMonth"
},
"email": {
"smtpHost": "smtp.qq.com",
diff --git a/public/app.js b/public/app.js
index 2716fa8..53984b0 100644
--- a/public/app.js
+++ b/public/app.js
@@ -90,7 +90,7 @@ function displayList(items, container) {
标段编号: ${item.bidNo}
标段名称: ${item.bidName}
发布日期: ${item.date}
- ${item.budget.amount}${item.budget.unit}
+ ${item.winningBid.amount}${item.winningBid.unit}
${item.href ? `
查看详情 →` : ''}
`).join('')}
@@ -189,7 +189,7 @@ function displayReport(report, container) {
标段编号: ${project.bidNo || '-'}
标段名称: ${project.bidName || '-'}
发布日期: ${project.date}
- ${project.budget.amount}${project.budget.unit}
+ ${project.winningBid.amount}${project.winningBid.unit}
${project.url ? `
查看详情 →` : ''}
`).join('')}
@@ -319,7 +319,7 @@ async function exportReport() {
new Paragraph({
children: [
new TextRun({ text: '合同估算价: ', bold: true }),
- new TextRun({ text: `${project.budget.amount}${project.budget.unit}` })
+ new TextRun({ text: `${project.winningBid.amount}${project.winningBid.unit}` })
],
spacing: { after: 50 }
}),
@@ -597,7 +597,8 @@ async function loadSchedulerConfig() {
document.getElementById('schedulerEnabled').checked = config.scheduler.enabled || false;
const cronTime = config.scheduler.cronTime || '0 9 * * *';
document.getElementById('schedulerCronInput').value = cronTime;
- document.getElementById('schedulerThresholdInput').value = config.scheduler.threshold || 10000;
+ document.getElementById('schedulerWinningThresholdInput').value = config.scheduler.winningThreshold !== undefined ? config.scheduler.winningThreshold : 10000;
+ document.getElementById('schedulerBidThresholdInput').value = config.scheduler.bidThreshold !== undefined ? config.scheduler.bidThreshold : 0;
document.getElementById('schedulerDescription').value = config.scheduler.description || '';
// 时间段配置
@@ -705,8 +706,20 @@ async function updateSchedulerStatus() {
// 更新执行计划
if (status.config) {
document.getElementById('schedulerCronTime').textContent = cronToFriendlyText(status.config.cronTime);
- const thresholdBillion = (status.config.threshold / 10000).toFixed(1);
- document.getElementById('schedulerThreshold').textContent = `${status.config.threshold}万元 (${thresholdBillion}亿)`;
+ const winningThreshold = status.config.winningThreshold;
+ if (winningThreshold === 0) {
+ document.getElementById('schedulerWinningThreshold').textContent = '不筛选';
+ } else {
+ const winningBillion = (winningThreshold / 10000).toFixed(1);
+ document.getElementById('schedulerWinningThreshold').textContent = `${winningThreshold}万元 (${winningBillion}亿)`;
+ }
+ const bidThreshold = status.config.bidThreshold;
+ if (bidThreshold === 0) {
+ document.getElementById('schedulerBidThreshold').textContent = '不筛选';
+ } else {
+ const bidBillion = (bidThreshold / 10000).toFixed(1);
+ document.getElementById('schedulerBidThreshold').textContent = `${bidThreshold}万元 (${bidBillion}亿)`;
+ }
}
}
} catch (error) {
@@ -719,7 +732,8 @@ async function saveSchedulerConfig() {
const schedulerConfig = {
enabled: document.getElementById('schedulerEnabled').checked,
cronTime: document.getElementById('schedulerCronInput').value,
- threshold: parseInt(document.getElementById('schedulerThresholdInput').value),
+ winningThreshold: parseInt(document.getElementById('schedulerWinningThresholdInput').value),
+ bidThreshold: parseInt(document.getElementById('schedulerBidThresholdInput').value),
description: document.getElementById('schedulerDescription').value,
timeRange: document.getElementById('schedulerTimeRange').value
};
@@ -834,3 +848,306 @@ function showSchedulerStatus(message, type) {
}, 3000);
}
}
+
+// ========== 招标公告列表功能 ==========
+let currentBidListPage = 1;
+
+// 获取招标公告列表
+async function fetchBidList(pageNum) {
+ const page = pageNum || parseInt(document.getElementById('bidListPage').value) || 1;
+ const loading = document.getElementById('bidListLoading');
+ const results = document.getElementById('bidListResults');
+ const pagination = document.getElementById('bidListPagination');
+
+ currentBidListPage = page;
+ document.getElementById('bidListPage').value = page;
+
+ loading.classList.add('active');
+ results.innerHTML = '';
+ pagination.style.display = 'none';
+
+ try {
+ const response = await fetch(`${API_BASE}/bid-announce/list?page=${page}`);
+ const data = await response.json();
+
+ if (data.success) {
+ displayBidList(data.data, results);
+ updateBidListPagination(page, data.data.length > 0);
+ } else {
+ results.innerHTML = `错误: ${data.error}
`;
+ }
+ } catch (error) {
+ results.innerHTML = `请求失败: ${error.message}
`;
+ } finally {
+ loading.classList.remove('active');
+ }
+}
+
+function goToBidListPage(page) {
+ if (page < 1) return;
+ fetchBidList(page);
+}
+
+function updateBidListPagination(page, hasData) {
+ const pagination = document.getElementById('bidListPagination');
+ const currentPageSpan = document.getElementById('bidCurrentPage');
+ const prevBtn = document.getElementById('bidPrevPage');
+ const firstBtn = document.getElementById('bidFirstPage');
+ const nextBtn = document.getElementById('bidNextPage');
+
+ if (hasData) {
+ pagination.style.display = 'flex';
+ currentPageSpan.textContent = page;
+
+ prevBtn.disabled = page <= 1;
+ firstBtn.disabled = page <= 1;
+ nextBtn.disabled = !hasData;
+ }
+}
+
+function displayBidList(items, container) {
+ if (items.length === 0) {
+ container.innerHTML = '没有找到招标公告
';
+ return;
+ }
+
+ const html = `
+
+
找到 ${items.length} 条招标公告
+ ${items.map((item, index) => `
+
+
${index + 1}. ${item.title}
+
发布日期: ${item.date}
+ ${item.href ? `
查看详情 →` : ''}
+
+ `).join('')}
+
+ `;
+
+ container.innerHTML = html;
+}
+
+// ========== 综合报告功能 ==========
+let currentWinningReport = null;
+let currentBidReport = null;
+
+// 初始化报告日期
+function initReportDates() {
+ const today = new Date();
+ const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
+
+ document.getElementById('startDate').value = firstDayOfMonth.toISOString().split('T')[0];
+ document.getElementById('endDate').value = today.toISOString().split('T')[0];
+}
+
+// 生成综合报告(同时包含中标和招标)
+async function generateCombinedReport() {
+ const startDate = document.getElementById('startDate').value;
+ const endDate = document.getElementById('endDate').value;
+ const maxPages = parseInt(document.getElementById('maxPages').value) || 10;
+ const winningThreshold = parseFloat(document.getElementById('reportThreshold').value) * 10000 || 0; // 转换为元
+ const bidThreshold = parseFloat(document.getElementById('bidReportThreshold').value) * 10000 || 0;
+
+ if (!startDate && !endDate) {
+ alert('请至少填写开始日期或结束日期');
+ return;
+ }
+
+ const loading = document.getElementById('reportLoading');
+ const loadingText = document.getElementById('reportLoadingText');
+ const results = document.getElementById('reportResults');
+ const sendBtn = document.getElementById('sendEmailBtn');
+
+ loading.classList.add('active');
+ results.innerHTML = '';
+ sendBtn.style.display = 'none';
+
+ try {
+ // 1. 先获取中标报告
+ loadingText.textContent = '正在采集中标公示...';
+ const winningResponse = await fetch(`${API_BASE}/report-daterange`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ startDate, endDate, threshold: winningThreshold, maxPages })
+ });
+ const winningData = await winningResponse.json();
+
+ // 2. 再获取招标报告
+ loadingText.textContent = '正在采集招标公告...';
+ const bidResponse = await fetch(`${API_BASE}/bid-announce/report`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ startDate, endDate, maxPages, threshold: bidThreshold })
+ });
+ const bidData = await bidResponse.json();
+
+ // 3. 显示综合报告
+ if (winningData.success && bidData.success) {
+ currentWinningReport = winningData.data;
+ currentBidReport = bidData.data;
+ displayCombinedReport(winningData.data, bidData.data, results);
+ sendBtn.style.display = 'inline-block';
+ } else {
+ let errorMsg = '';
+ if (!winningData.success) errorMsg += `中标报告错误: ${winningData.error}\n`;
+ if (!bidData.success) errorMsg += `招标报告错误: ${bidData.error}`;
+ results.innerHTML = `${errorMsg}
`;
+ }
+ } catch (error) {
+ results.innerHTML = `请求失败: ${error.message}
`;
+ } finally {
+ loading.classList.remove('active');
+ loadingText.textContent = '正在生成报告...';
+ }
+}
+
+// 显示综合报告
+function displayCombinedReport(winningReport, bidReport, container) {
+ const html = `
+
+
+
中标公示报告
+
+
总项目数
+
${winningReport.summary.total_count}
+
+
+
符合条件
+
${winningReport.summary.filtered_count}
+
+
+
总金额
+
${winningReport.summary.total_amount}
+
+
+
阈值
+
${winningReport.summary.threshold}
+
+
+
+ ${winningReport.projects.length === 0 ? '暂无符合条件的中标项目
' : `
+ 中标项目列表 (${winningReport.projects.length} 条)
+
+ ${winningReport.projects.map((project, index) => `
+
+
${index + 1}. ${project.title}
+
标段编号: ${project.bidNo || '-'}
+
标段名称: ${project.bidName || '-'}
+
发布日期: ${project.date}
+
${project.winningBid.amount}${project.winningBid.unit}
+ ${project.url ? `
查看详情 →` : ''}
+
+ `).join('')}
+
+ `}
+
+
+
+
招标公告报告
+
+
总公告数量
+
${bidReport.summary.total_count} 条
+
+
+
有金额信息
+
${bidReport.summary.has_amount_count || bidReport.summary.filtered_count} 条
+
+
+
金额阈值
+
${bidReport.summary.threshold || '无'}
+
+
+
合同估算总额
+
${bidReport.summary.total_amount}
+
+
+
+ ${bidReport.projects.length === 0 ? '暂无符合条件的招标项目
' : `
+ 招标项目详情 (${bidReport.projects.length} 条)
+
+ ${bidReport.projects.map((item, index) => `
+
+
${index + 1}. ${item.title}
+
发布日期: ${item.date}
+ ${item.bidCode ? `
标段编码: ${item.bidCode}
` : ''}
+ ${item.tenderee ? `
招标人: ${item.tenderee}
` : ''}
+ ${item.duration ? `
工期: ${item.duration} 日历天
` : ''}
+ ${item.estimatedAmount ? `
+
合同估算价: ${item.estimatedAmount.amountWan} 万元
+ ` : ''}
+
查看详情 →
+
+ `).join('')}
+
+ `}
+ `;
+
+ container.innerHTML = html;
+}
+
+// 发送综合报告邮件
+async function sendCombinedReportByEmail() {
+ if (!currentWinningReport && !currentBidReport) {
+ alert('请先生成报告');
+ return;
+ }
+
+ // 从localStorage获取邮件配置
+ const emailConfigStr = localStorage.getItem('emailConfig');
+ if (!emailConfigStr) {
+ alert('请先在"邮件配置"页面配置邮件服务器信息');
+ return;
+ }
+
+ let emailConfig;
+ try {
+ emailConfig = JSON.parse(emailConfigStr);
+ } catch (e) {
+ alert('邮件配置解析失败,请重新配置');
+ return;
+ }
+
+ if (!emailConfig.smtpHost || !emailConfig.smtpUser || !emailConfig.smtpPass || !emailConfig.recipients) {
+ alert('邮件配置不完整,请检查SMTP服务器、用户名、密码和收件人');
+ return;
+ }
+
+ if (!confirm(`确定要将综合报告发送到以下邮箱吗?\n\n${emailConfig.recipients}`)) {
+ return;
+ }
+
+ const sendBtn = document.getElementById('sendEmailBtn');
+ const originalText = sendBtn.textContent;
+ sendBtn.disabled = true;
+ sendBtn.textContent = '正在发送...';
+
+ try {
+ const response = await fetch(`${API_BASE}/send-combined-email`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ emailConfig,
+ winningReport: currentWinningReport,
+ bidReport: currentBidReport
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ alert('综合报告邮件发送成功!');
+ } else {
+ alert(`邮件发送失败: ${data.error}`);
+ }
+ } catch (error) {
+ alert(`请求失败: ${error.message}`);
+ } finally {
+ sendBtn.disabled = false;
+ sendBtn.textContent = originalText;
+ }
+}
+
+// 页面加载时初始化报告日期
+document.addEventListener('DOMContentLoaded', function() {
+ initReportDates();
+});
diff --git a/public/index.html b/public/index.html
index a1c2695..d96266a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,7 +3,7 @@
- 南京公共资源交易平台 - 合同估算价采集工具
+ 南京公共资源交易平台 - 中标价格采集工具
+
+
+
+
南京公共资源交易平台 - 交通水务招标公告报告
+
+
+
报告摘要
+
+
+
总公告数量
+
${summary.total_count} 条
+
+
+
有金额信息
+
${summary.has_amount_count || summary.filtered_count} 条
+
+
+
符合筛选
+
${summary.filtered_count} 条
+
+
+
金额阈值
+
${summary.threshold || '无'}
+
+
+
合同估算总额
+
${summary.total_amount}
+
+
+ ${summary.date_range ? `
+
+
时间范围
+
+ ${summary.date_range.startDate || '不限'} 至 ${summary.date_range.endDate || '不限'}
+
+
+ ` : ''}
+
+
+
招标项目详情
+
+ ${projects.length === 0 ? '
暂无符合条件的项目
' : ''}
+ ${projects.map((project, index) => `
+
+
${index + 1}. ${project.title}
+
+ 发布日期: ${project.date}
+ ${project.bidCode ? ` | 标段编码: ${project.bidCode}` : ''}
+
+ ${project.tenderee ? `
+
+ 招标人: ${project.tenderee}
+ ${project.duration ? ` | 工期: ${project.duration}日历天` : ''}
+
+ ` : ''}
+ ${project.estimatedAmount ? `
+
+ 合同估算价: ${project.estimatedAmount.amountWan} 万元
+
+ ` : ''}
+
+
+ `).join('')}
+
+
+
+
+
+