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:
Jowe
2026-02-06 15:54:13 +08:00
parent c1a06ad684
commit 939717fa57
27 changed files with 1670 additions and 140 deletions

View File

@@ -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>