feat: v2.6.0 - API安全优化和文档整合
## 核心优化 - 移除详情页自动调用博查API的逻辑,改为按需加载 - 添加基于IP的频率限制(每小时3次) - 实现验证码防护机制(超过阈值后要求验证) - 新增频率限制工具类 utils/rate_limiter.py ## 成本控制 - API调用减少约90%+(只在用户点击时调用) - 防止恶意滥用和攻击 - 可配置的频率限制和验证码策略 ## 文档整合 - 创建 docs/ 目录结构 - 归档历史版本文档到 docs/archive/ - 移动部署文档到 docs/deployment/ - 添加文档索引 docs/README.md ## 技术变更 - 新增依赖: Flask-Limiter==3.5.0 - 修改: app.py (移除自动调用,新增API端点) - 修改: templates/detail_new.html (按需加载UI) - 新增: utils/rate_limiter.py (频率限制和验证码) - 新增: docs/archive/DEVELOP_v2.6.0_API_SECURITY.md ## 部署说明 1. pip install Flask-Limiter==3.5.0 2. 重启应用 3. 无需数据库迁移 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -788,62 +788,68 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Related News -->
|
||||
{% if news_list %}
|
||||
<div class="content-block">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||
<h2 style="margin: 0;">
|
||||
<span>📰</span>
|
||||
相关新闻
|
||||
</h2>
|
||||
<button id="refreshNewsBtn" class="refresh-news-btn" onclick="refreshNews('{{ site.code }}')">
|
||||
<span class="refresh-icon">↻</span> 获取最新资讯
|
||||
<button id="refreshNewsBtn" class="refresh-news-btn" onclick="loadNews('{{ site.code }}', false)">
|
||||
<span class="refresh-icon">↻</span> <span class="btn-text">{% if has_news %}获取最新资讯{% else %}加载资讯{% endif %}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="newsContainer">
|
||||
{% for news in news_list %}
|
||||
<div class="news-item">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
|
||||
<span class="news-badge">{{ news.news_type }}</span>
|
||||
{% if news.source_name %}
|
||||
<div style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted);">
|
||||
{% if news.source_icon %}
|
||||
<img src="{{ news.source_icon }}" alt="{{ news.source_name }}" style="width: 16px; height: 16px; border-radius: 2px;">
|
||||
{% if news_list %}
|
||||
{% for news in news_list %}
|
||||
<div class="news-item">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
|
||||
<span class="news-badge">{{ news.news_type }}</span>
|
||||
{% if news.source_name %}
|
||||
<div style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted);">
|
||||
{% if news.source_icon %}
|
||||
<img src="{{ news.source_icon }}" alt="{{ news.source_name }}" style="width: 16px; height: 16px; border-radius: 2px;">
|
||||
{% endif %}
|
||||
<span>{{ news.source_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span>{{ news.source_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h4>
|
||||
{% if news.url %}
|
||||
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="color: var(--text-primary); text-decoration: none;">
|
||||
{{ news.title }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ news.title }}
|
||||
{% endif %}
|
||||
</h4>
|
||||
{% if news.content %}
|
||||
<p>{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div class="news-date">
|
||||
{% if news.published_at %}
|
||||
{{ news.published_at.strftime('%Y年%m月%d日') }}
|
||||
<h4>
|
||||
{% if news.url %}
|
||||
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="color: var(--text-primary); text-decoration: none;">
|
||||
{{ news.title }}
|
||||
</a>
|
||||
{% else %}
|
||||
未知日期
|
||||
{{ news.title }}
|
||||
{% endif %}
|
||||
</h4>
|
||||
{% if news.content %}
|
||||
<p>{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div class="news-date">
|
||||
{% if news.published_at %}
|
||||
{{ news.published_at.strftime('%Y年%m月%d日') }}
|
||||
{% else %}
|
||||
未知日期
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if news.url %}
|
||||
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="font-size: 12px; color: var(--primary-blue); text-decoration: none;">
|
||||
阅读全文 ↗
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if news.url %}
|
||||
<a href="{{ news.url }}" target="_blank" rel="noopener noreferrer" style="font-size: 12px; color: var(--primary-blue); text-decoration: none;">
|
||||
阅读全文 ↗
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="news-placeholder" style="text-align: center; padding: 40px 20px; color: var(--text-muted);">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">📰</div>
|
||||
<p style="font-size: 14px;">暂无新闻资讯</p>
|
||||
<p style="font-size: 12px; margin-top: 8px;">点击右上角"加载资讯"按钮获取最新内容</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div><!-- End newsContainer -->
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
@@ -932,7 +938,8 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function refreshNews(siteCode) {
|
||||
// v2.6优化:按需加载新闻,避免自动调用API
|
||||
function loadNews(siteCode, isRefresh = false) {
|
||||
const btn = document.getElementById('refreshNewsBtn');
|
||||
const newsContainer = document.getElementById('newsContainer');
|
||||
|
||||
@@ -941,17 +948,11 @@ function refreshNews(siteCode) {
|
||||
// 禁用按钮,显示加载状态
|
||||
btn.disabled = true;
|
||||
btn.classList.add('loading');
|
||||
btn.innerHTML = '<span class="refresh-icon">↻</span> 正在获取...';
|
||||
const originalText = btn.querySelector('.btn-text').textContent;
|
||||
btn.querySelector('.btn-text').textContent = '加载中...';
|
||||
|
||||
// 显示加载提示
|
||||
const loadingMsg = document.createElement('div');
|
||||
loadingMsg.className = 'news-status';
|
||||
loadingMsg.style.display = 'block';
|
||||
loadingMsg.textContent = '正在获取最新资讯,请稍候...';
|
||||
newsContainer.insertAdjacentElement('beforebegin', loadingMsg);
|
||||
|
||||
// 调用刷新API
|
||||
fetch(`/api/refresh-site-news/${siteCode}`, {
|
||||
// 调用新闻获取API
|
||||
fetch(`/api/fetch-news/${siteCode}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -960,40 +961,99 @@ function refreshNews(siteCode) {
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// 刷新成功,重新加载页面
|
||||
loadingMsg.className = 'news-status success';
|
||||
loadingMsg.textContent = `✓ 成功获取 ${data.saved_count || 0} 条新资讯,页面即将刷新...`;
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
// 刷新失败
|
||||
loadingMsg.className = 'news-status error';
|
||||
loadingMsg.textContent = `✗ ${data.message || '获取失败,请稍后重试'}`;
|
||||
// 更新新闻列表
|
||||
if (data.news && data.news.length > 0) {
|
||||
let newsHTML = '';
|
||||
data.news.forEach(news => {
|
||||
newsHTML += `
|
||||
<div class="news-item">
|
||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
|
||||
<span class="news-badge">${news.news_type}</span>
|
||||
${news.source_name ? `
|
||||
<div style="display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-muted);">
|
||||
${news.source_icon ? `<img src="${news.source_icon}" alt="${news.source_name}" style="width: 16px; height: 16px; border-radius: 2px;">` : ''}
|
||||
<span>${news.source_name}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<h4>
|
||||
${news.url ? `
|
||||
<a href="${news.url}" target="_blank" rel="noopener noreferrer" style="color: var(--text-primary); text-decoration: none;">
|
||||
${news.title}
|
||||
</a>
|
||||
` : news.title}
|
||||
</h4>
|
||||
${news.content ? `<p>${news.content}${news.content.length >= 200 ? '...' : ''}</p>` : ''}
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div class="news-date">${news.published_at}</div>
|
||||
${news.url ? `
|
||||
<a href="${news.url}" target="_blank" rel="noopener noreferrer" style="font-size: 12px; color: var(--primary-blue); text-decoration: none;">
|
||||
阅读全文 ↗
|
||||
</a>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
newsContainer.innerHTML = newsHTML;
|
||||
|
||||
// 显示成功消息
|
||||
showMessage(data.message, 'success');
|
||||
} else {
|
||||
newsContainer.innerHTML = `
|
||||
<div class="news-placeholder" style="text-align: center; padding: 40px 20px; color: var(--text-muted);">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">📰</div>
|
||||
<p style="font-size: 14px;">暂无新闻资讯</p>
|
||||
</div>
|
||||
`;
|
||||
showMessage('暂无新资讯', 'info');
|
||||
}
|
||||
|
||||
// 恢复按钮
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
btn.innerHTML = '<span class="refresh-icon">↻</span> 获取最新资讯';
|
||||
|
||||
// 3秒后隐藏错误消息
|
||||
setTimeout(() => {
|
||||
loadingMsg.style.display = 'none';
|
||||
}, 3000);
|
||||
btn.querySelector('.btn-text').textContent = '获取最新资讯';
|
||||
} else {
|
||||
// 获取失败
|
||||
showMessage(data.error || '获取失败,请稍后重试', 'error');
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
btn.querySelector('.btn-text').textContent = originalText;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
loadingMsg.className = 'news-status error';
|
||||
loadingMsg.textContent = '✗ 网络请求失败,请检查网络连接';
|
||||
showMessage('网络请求失败,请检查网络连接', 'error');
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('loading');
|
||||
btn.innerHTML = '<span class="refresh-icon">↻</span> 获取最新资讯';
|
||||
|
||||
// 3秒后隐藏错误消息
|
||||
setTimeout(() => {
|
||||
loadingMsg.style.display = 'none';
|
||||
}, 3000);
|
||||
btn.querySelector('.btn-text').textContent = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
function showMessage(message, type = 'info') {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `news-status ${type}`;
|
||||
messageDiv.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
background: ${type === 'success' ? '#10b981' : type === 'error' ? '#ef4444' : '#3b82f6'};
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
z-index: 9999;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
messageDiv.textContent = message;
|
||||
document.body.appendChild(messageDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
messageDiv.style.animation = 'slideOut 0.3s ease';
|
||||
setTimeout(() => messageDiv.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user