diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..908caae --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "document-skills@anthropic-agent-skills": true + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2b47cb6..4fb503d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -33,7 +33,17 @@ "Bash(ls:*)", "Bash(git pull:*)", "Bash(del nul)", - "Bash(git checkout:*)" + "Bash(git checkout:*)", + "Bash(git push:*)", + "Bash(netstat:*)", + "Bash(git config:*)", + "Bash(taskkill:*)", + "Bash(cmd /c:*)", + "Bash(powershell:*)", + "Bash(ssh:*)", + "Bash(start:*)", + "Bash(git status --porcelain=v1)", + "Bash(timeout 3 cmd:*)" ] } } diff --git a/app.py b/app.py index 3445a7f..e713840 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,7 @@ from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTe from utils.website_fetcher import WebsiteFetcher from utils.tag_generator import TagGenerator from utils.news_searcher import NewsSearcher +from utils.rate_limiter import get_rate_limiter, get_client_ip, CaptchaVerifier def create_app(config_name='default'): """应用工厂函数""" @@ -156,70 +157,8 @@ def create_app(config_name='default'): site.view_count += 1 db.session.commit() - # 智能新闻更新:检查今天是否已更新过新闻 - from datetime import date - today = date.today() - - # 检查该网站最新一条新闻的创建时间 - latest_news = News.query.filter_by( - site_id=site.id - ).order_by(News.created_at.desc()).first() - - # 判断是否需要更新新闻 - need_update = False - if not latest_news: - # 没有任何新闻,需要获取 - need_update = True - elif latest_news.created_at.date() < today: - # 最新新闻不是今天创建的,需要更新 - need_update = True - - # 如果需要更新,自动获取最新新闻 - if need_update: - api_key = app.config.get('BOCHA_API_KEY') - if api_key: - try: - # 创建新闻搜索器 - searcher = NewsSearcher(api_key) - - # 获取新闻(限制3条,一周内的) - news_items = searcher.search_site_news( - site_name=site.name, - site_url=site.url, - news_keywords=site.news_keywords, # v2.3新增:使用专用关键词 - count=3, - freshness='oneWeek' - ) - - # 保存新闻到数据库 - if news_items: - for item in news_items: - # 检查是否已存在(根据URL去重) - existing = News.query.filter_by( - site_id=site.id, - url=item['url'] - ).first() - - if not existing: - news = News( - site_id=site.id, - title=item['title'], - content=item.get('summary') or item.get('snippet', ''), - url=item['url'], - source_name=item.get('site_name', ''), - source_icon=item.get('site_icon', ''), - published_at=item.get('published_at'), - news_type='Search Result', - is_active=True - ) - db.session.add(news) - - db.session.commit() - - except Exception as e: - # 获取新闻失败,不影响页面显示 - print(f"自动获取新闻失败:{str(e)}") - db.session.rollback() + # v2.6优化:移除自动调用博查API的逻辑,改为按需加载 + # 只获取数据库中已有的新闻,不再自动调用API # 获取该网站的相关新闻(最多显示5条) news_list = News.query.filter_by( @@ -227,6 +166,9 @@ def create_app(config_name='default'): is_active=True ).order_by(News.published_at.desc()).limit(5).all() + # 检查是否有新闻,如果没有则标记需要加载 + has_news = len(news_list) > 0 + # 获取同类工具推荐(通过标签匹配,最多显示4个) recommended_sites = [] if site.tags: @@ -237,7 +179,148 @@ def create_app(config_name='default'): Site.tags.any(Tag.id.in_([tag.id for tag in site.tags])) ).order_by(Site.view_count.desc()).limit(4).all() - return render_template('detail_new.html', site=site, news_list=news_list, recommended_sites=recommended_sites) + return render_template('detail_new.html', site=site, news_list=news_list, has_news=has_news, recommended_sites=recommended_sites) + + # ========== 新闻相关API (v2.6优化) ========== + @app.route('/api/fetch-news/', methods=['POST']) + def fetch_news_for_site(code): + """ + 按需获取指定网站的新闻 + v2.6优化:添加频率限制和验证码防护,避免API被滥用 + """ + try: + # 获取客户端IP + client_ip = get_client_ip(request) + + # 获取频率限制器 + limiter = get_rate_limiter() + + # 检查是否需要验证码 + captcha_required, captcha_reason = limiter.is_captcha_required(client_ip) + if captcha_required: + return jsonify({ + 'success': False, + 'error': captcha_reason, + 'require_captcha': True + }), 429 + + # 检查频率限制(每小时3次) + is_limited, remaining, reset_time = limiter.is_rate_limited( + client_ip, + action='news_fetch', + limit=3, + window_minutes=60 + ) + + if is_limited: + # 触发频率限制,要求验证码 + limiter.require_captcha(client_ip, duration_minutes=30) + return jsonify({ + 'success': False, + 'error': f'请求过于频繁,请在 {reset_time.strftime("%H:%M")} 后重试', + 'require_captcha': True, + 'reset_time': reset_time.isoformat() + }), 429 + + # 检查验证码(如果提供了) + captcha_token = request.json.get('captcha_token') if request.json else None + if captcha_token: + # TODO: 配置实际的验证码服务(reCAPTCHA或hCaptcha) + verifier = CaptchaVerifier(service='simple') + success, error = verifier.verify(captcha_token, client_ip) + if success: + # 验证通过,清除验证码要求 + limiter.clear_captcha_requirement(client_ip) + else: + return jsonify({ + 'success': False, + 'error': f'验证码验证失败: {error}' + }), 400 + + # 记录本次请求 + limiter.record_request(client_ip, action='news_fetch') + + # 查找网站 + site = Site.query.filter_by(code=code, is_active=True).first_or_404() + + # 检查博查API配置 + api_key = app.config.get('BOCHA_API_KEY') + if not api_key: + return jsonify({'success': False, 'error': '博查API未配置'}), 500 + + # 创建新闻搜索器 + searcher = NewsSearcher(api_key) + + # 获取新闻(限制5条,一周内的) + news_items = searcher.search_site_news( + site_name=site.name, + site_url=site.url, + news_keywords=site.news_keywords, + count=5, + freshness='oneWeek' + ) + + # 保存新闻到数据库 + new_count = 0 + if news_items: + for item in news_items: + # 检查是否已存在(根据URL去重) + existing = News.query.filter_by( + site_id=site.id, + url=item['url'] + ).first() + + if not existing: + news = News( + site_id=site.id, + title=item['title'], + content=item.get('summary') or item.get('snippet', ''), + url=item['url'], + source_name=item.get('site_name', ''), + source_icon=item.get('site_icon', ''), + published_at=item.get('published_at'), + news_type='Search Result', + is_active=True + ) + db.session.add(news) + new_count += 1 + + db.session.commit() + + # 返回最新的新闻列表 + news_list = News.query.filter_by( + site_id=site.id, + is_active=True + ).order_by(News.published_at.desc()).limit(5).all() + + # 格式化新闻数据 + news_data = [] + for news in news_list: + news_data.append({ + 'title': news.title, + 'content': news.content[:200] if news.content else '', + 'url': news.url, + 'source_name': news.source_name, + 'source_icon': news.source_icon, + 'published_at': news.published_at.strftime('%Y年%m月%d日') if news.published_at else '未知日期', + 'news_type': news.news_type + }) + + return jsonify({ + 'success': True, + 'news': news_data, + 'new_count': new_count, + 'remaining_requests': remaining - 1, # 已经消耗了一次 + 'message': f'成功获取{new_count}条新资讯' if new_count > 0 else '暂无新资讯' + }) + + except Exception as e: + print(f"获取新闻失败:{str(e)}") + import traceback + traceback.print_exc() + db.session.rollback() + return jsonify({'success': False, 'error': str(e)}), 500 + # ========== 社媒营销路由 (v2.5新增) ========== @app.route('/api/generate-social-share', methods=['POST']) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..072b6d9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,42 @@ +# ZJPB 项目文档 + +## 📚 文档索引 + +### 当前版本文档 +- [README.md](../README.md) - 项目总体介绍 +- [DEPLOY_INFO.md](../DEPLOY_INFO.md) - 当前服务器部署信息(v2.5+) + +### 部署文档 +- [deployment/](./deployment/) - 通用部署指南 + - `DEPLOYMENT.md` - 完整部署流程 + - `QUICK_DEPLOY.md` - 快速部署指南 + - `1PANEL_DEPLOY.md` - 1Panel部署方案 + - `INCREMENTAL_DEPLOY.md` - 增量部署方案 + - `MANUAL_DEPLOY.md` - 手动部署方案 + +### 历史版本文档 +- [archive/](./archive/) - 历史版本开发与部署文档 + - `DEVELOP_v2.4.1_TAB_FEATURE.md` - v2.4.1标签功能文档 + - `NEWS_FEATURE_v2.2.md` - v2.2新闻功能文档 + - `DEPLOY_v2.x.md` - 各版本部署文档 + +## 📝 文档整合说明 + +**整合日期**: 2026-02-06 +**整合原因**: 简化文档结构,提高可维护性 + +### 目录结构 +``` +docs/ +├── README.md # 本文档 +├── deployment/ # 部署相关文档 +└── archive/ # 历史版本文档 +``` + +### 使用建议 +1. 新项目开发者请先阅读根目录的 `README.md` +2. 部署时参考 `DEPLOY_INFO.md` 和 `docs/deployment/` +3. 了解历史功能请查阅 `docs/archive/` + +--- +**维护**: 每次重大更新后请更新此索引 diff --git a/DEPLOY_CHECKLIST.md b/docs/archive/DEPLOY_CHECKLIST.md similarity index 100% rename from DEPLOY_CHECKLIST.md rename to docs/archive/DEPLOY_CHECKLIST.md diff --git a/DEPLOY_CHECKLIST_v2.3.md b/docs/archive/DEPLOY_CHECKLIST_v2.3.md similarity index 100% rename from DEPLOY_CHECKLIST_v2.3.md rename to docs/archive/DEPLOY_CHECKLIST_v2.3.md diff --git a/DEPLOY_v2.2.0.md b/docs/archive/DEPLOY_v2.2.0.md similarity index 100% rename from DEPLOY_v2.2.0.md rename to docs/archive/DEPLOY_v2.2.0.md diff --git a/DEPLOY_v2.2_CHECKLIST.md b/docs/archive/DEPLOY_v2.2_CHECKLIST.md similarity index 100% rename from DEPLOY_v2.2_CHECKLIST.md rename to docs/archive/DEPLOY_v2.2_CHECKLIST.md diff --git a/DEPLOY_v2.2_QUICK.md b/docs/archive/DEPLOY_v2.2_QUICK.md similarity index 100% rename from DEPLOY_v2.2_QUICK.md rename to docs/archive/DEPLOY_v2.2_QUICK.md diff --git a/DEPLOY_v2.3.0.md b/docs/archive/DEPLOY_v2.3.0.md similarity index 100% rename from DEPLOY_v2.3.0.md rename to docs/archive/DEPLOY_v2.3.0.md diff --git a/DEPLOY_v2.3_QUICK.md b/docs/archive/DEPLOY_v2.3_QUICK.md similarity index 100% rename from DEPLOY_v2.3_QUICK.md rename to docs/archive/DEPLOY_v2.3_QUICK.md diff --git a/DEPLOY_v2.4.0.md b/docs/archive/DEPLOY_v2.4.0.md similarity index 100% rename from DEPLOY_v2.4.0.md rename to docs/archive/DEPLOY_v2.4.0.md diff --git a/docs/archive/DEVELOP_v2.4.1_TAB_FEATURE.md b/docs/archive/DEVELOP_v2.4.1_TAB_FEATURE.md new file mode 100644 index 0000000..1d31eac --- /dev/null +++ b/docs/archive/DEVELOP_v2.4.1_TAB_FEATURE.md @@ -0,0 +1,594 @@ +# ZJPB v2.4.1 开发文档 - 标签功能优化 + +**版本号**: v2.4.1 +**开发日期**: 2026-01-04 +**功能主题**: 最新/热门/推荐标签功能 +**开发者**: Claude Code +**Git Commit**: 8011e5b + +--- + +## 📋 功能概述 + +### 用户需求 +用户原本要求实现"热门工具排行榜"功能,但在看到初版实现后明确拒绝了顶部独立排行榜的设计,提出了新的需求: + +> "这个列表并不好看,不用在顶部单独增加模块,可以在tag下增加3个tab,分别是"最新"、"热门"、"推荐" 这三个,其中"推荐"可以在后台添加网站或者编辑的时候设置" + +### 最终实现 +- ✅ **完全移除**:删除顶部热门工具排行榜模块(约118行CSS + HTML) +- ✅ **Tab导航**:在分类标签下方添加三个tab(最新/热门/推荐) +- ✅ **数据库支持**:添加`is_recommended`字段到Site模型 +- ✅ **后台管理**:支持在后台标记推荐工具 +- ✅ **状态保持**:URL参数保持tab、分类、搜索、分页状态 +- ✅ **完整测试**:所有功能已本地验证通过 + +--- + +## 🏗️ 技术架构 + +### 1. 数据库层 + +#### 新增字段 +**表名**: `sites` +**字段名**: `is_recommended` +**类型**: `TINYINT(1)` +**默认值**: `0` (False) +**注释**: "是否推荐" + +**DDL语句**: +```sql +ALTER TABLE sites ADD COLUMN is_recommended TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否推荐'; +``` + +#### 迁移脚本 +**文件**: `migrations/add_is_recommended.py` +**特性**: +- 幂等性检查(避免重复执行) +- 支持upgrade/downgrade +- 自动检测字段是否已存在 +- 错误处理和事务回滚 + +### 2. 后端层 + +#### 路由修改 +**文件**: `app.py` +**函数**: `index()` (Lines 70-148) + +**新增参数处理**: +```python +current_tab = request.args.get('tab', 'latest') # 默认为"最新" +``` + +**三种模式的查询逻辑**: + +1. **最新模式** (默认): +```python +if current_tab == 'latest' or not current_tab: + query = query.order_by(Site.created_at.desc(), Site.id.desc()) +``` + +2. **热门模式**: +```python +elif current_tab == 'popular': + query = query.order_by(Site.view_count.desc(), Site.id.desc()) +``` + +3. **推荐模式**: +```python +elif current_tab == 'recommended': + query = query.filter_by(is_recommended=True).order_by(Site.sort_order.desc(), Site.id.desc()) +``` + +**关键特性**: +- 支持与分类筛选(tag)组合使用 +- 支持与搜索(q)组合使用 +- 支持分页 +- 所有查询条件可叠加 + +#### Flask-Admin配置 +**文件**: `app.py` +**类**: `SiteAdmin` (Lines 1345-1481) + +**修改点**: +```python +# 列表显示添加推荐字段 +column_list = ['id', 'code', 'name', 'url', 'slug', 'is_active', 'is_recommended', 'view_count', 'created_at'] + +# 添加推荐筛选器 +column_filters = ['is_active', 'is_recommended', 'tags'] + +# 表单添加推荐字段 +form_columns = ['name', 'url', 'slug', 'logo', 'short_desc', 'description', 'features', 'news_keywords', 'tags', 'is_active', 'is_recommended', 'sort_order'] + +# 中文标签 +column_labels = { + 'is_recommended': '是否推荐', + # ... 其他字段 +} +``` + +### 3. 前端层 + +#### 模板文件 +**文件**: `templates/index_new.html` + +**删除内容** (约118行): +- `.popular-section` 相关所有CSS +- `.popular-header`, `.popular-title`, `.popular-badge` 样式 +- `.popular-grid`, `.popular-item` 样式 +- 顶部热门工具HTML结构 + +**新增内容**: + +**Tab导航CSS** (Lines 339-397): +```css +.sort-tabs { + padding: 24px 0 0 0; + border-top: 1px solid var(--border-color); + margin-top: 24px; +} + +.sort-tab { + padding: 8px 20px; + border-radius: 50px; + /* ... 完整样式见源码 */ +} + +.sort-tab.active { + background: var(--primary-blue); + border-color: var(--primary-blue); + color: white; +} +``` + +**Tab导航HTML** (Lines 443-462): +```html +
+ +
+``` + +**分页链接更新** (Lines 505-556): +所有分页链接都更新为保持tab状态: +```html + + +``` + +**关键逻辑**: +- 默认tab为'latest'时不显示在URL中(保持URL简洁) +- 非默认tab时添加`&tab=xxx`参数 +- 保持所有其他状态(tag, q, page) + +--- + +## 📁 文件变更清单 + +### 新增文件 +1. **migrations/add_is_recommended.py** (73 lines) + - 数据库迁移脚本 + - 添加is_recommended字段 + +### 修改文件 +1. **models.py** + - Line 29: 添加`is_recommended`字段定义 + - Line 56: `to_dict()`方法添加字段序列化 + +2. **app.py** + - Lines 95: 添加`current_tab`参数处理 + - Lines 130-139: 实现三种tab模式的查询逻辑 + - Line 148: 传递`current_tab`到模板 + - Lines 1360-1382: Flask-Admin配置更新 + +3. **templates/index_new.html** + - 删除: ~118行热门section相关代码 + - Lines 339-397: 新增tab导航CSS + - Lines 443-462: 新增tab导航HTML + - Lines 505-556: 更新所有分页链接 + +**统计**: 4个文件修改,167行新增,181行删除 + +--- + +## 🔄 URL参数设计 + +### 参数说明 +| 参数 | 说明 | 默认值 | 示例 | +|------|------|--------|------| +| `tag` | 分类筛选 | 无 | `tag=ai-chat` | +| `tab` | 排序模式 | `latest` | `tab=popular` | +| `q` | 搜索关键词 | 无 | `q=chatgpt` | +| `page` | 页码 | `1` | `page=2` | + +### URL组合示例 +``` +# 只有tab +/?tab=popular + +# 分类 + tab +/?tag=ai-chat&tab=popular + +# 分类 + tab + 分页 +/?tag=ai-chat&tab=recommended&page=2 + +# 分类 + tab + 搜索 + 分页 +/?tag=ai-chat&tab=popular&q=对话&page=2 +``` + +### 设计原则 +- **简洁性**: 默认值(tab=latest, page=1)不出现在URL中 +- **状态保持**: 所有操作(切换tab、翻页等)保持其他参数 +- **向后兼容**: 无tab参数时默认为latest模式 +- **SEO友好**: URL清晰可读 + +--- + +## 🧪 测试验证 + +### 本地测试(已完成) + +**测试环境**: +- Flask Development Server +- 测试时间: 2026-01-04 00:16:41 - 00:54:25 +- 数据库: MySQL + +**测试用例**: + +1. ✅ **数据库迁移** + - 执行时间: 00:28:18 + - 结果: 成功添加is_recommended字段 + - SQL: `ALTER TABLE sites ADD COLUMN is_recommended TINYINT(1) NOT NULL DEFAULT 0` + +2. ✅ **三种tab模式** + - 最新 (00:38:37): `ORDER BY created_at DESC` + - 热门 (00:38:39): `ORDER BY view_count DESC` + - 推荐 (00:39:10): `WHERE is_recommended = true ORDER BY sort_order DESC` + +3. ✅ **后台管理** + - 访问admin界面 (00:38:48) + - 编辑Site ID=1 (00:39:02) + - 成功设置is_recommended=1 + +4. ✅ **组合筛选** + - 分类 + tab: `?tag=ai-chat&tab=popular` (00:39:15) + - 所有组合均正常工作 + +5. ✅ **分页状态保持** + - URL参数在翻页时正确保持 + +### 生产部署(已完成) + +**部署时间**: 2026-01-04 +**部署方式**: Git pull + 数据库迁移 + 应用重启 +**部署状态**: ✅ 成功 + +--- + +## 💡 核心技术要点 + +### 1. SQLAlchemy查询链式调用 +```python +# 基础查询 +query = Site.query.filter_by(is_active=True) + +# 条件叠加 +if tag_slug: + query = query.filter(Site.tags.contains(selected_tag)) + +# 排序方式 +query = query.order_by(Site.created_at.desc(), Site.id.desc()) + +# 分页 +pagination = query.paginate(page=page, per_page=100, error_out=False) +``` + +### 2. Jinja2条件CSS类 +```html + +``` + +### 3. URL参数拼接 +```html +href="/?{% if selected_tag %}tag={{ selected_tag.slug }}&{% endif %}tab=latest" +``` + +**技巧**: 使用`{% if %}`控制参数是否出现,避免空参数 + +### 4. Flask请求参数处理 +```python +# 获取参数,提供默认值 +current_tab = request.args.get('tab', 'latest') + +# 安全获取整数 +page = request.args.get('page', 1, type=int) + +# 字符串处理 +search_query = request.args.get('q', '').strip() +``` + +--- + +## 🎨 UI/UX设计 + +### 视觉设计 + +**Tab样式**: +- 未激活: 白色背景,灰色边框,灰色文字 +- 悬停: 蓝色边框,浅蓝背景,蓝色文字 +- 激活: 蓝色背景,白色文字 +- 圆角: 50px(胶囊形状) +- 图标: Unicode emoji(🕐🔥⭐) + +**位置**: +- 在分类标签下方 +- 顶部有1px灰色分隔线 +- 24px上边距 + +### 交互设计 + +**用户流程**: +1. 用户访问首页,默认显示"最新"工具 +2. 点击分类标签,查看特定分类 +3. 切换tab,改变排序方式 +4. 所有状态通过URL保持,支持刷新和分享 + +**状态反馈**: +- 当前激活的tab高亮显示 +- URL参数实时更新 +- 页面内容即时切换 + +--- + +## 🔒 数据完整性 + +### 默认值处理 +- 所有现有记录的`is_recommended`默认为`0` (False) +- 新创建的Site默认`is_recommended=False` +- 不影响现有数据 + +### 查询优化 +```python +# 推荐模式只查询is_recommended=True的记录 +query.filter_by(is_recommended=True) + +# 使用索引字段排序 +order_by(Site.sort_order.desc(), Site.id.desc()) +``` + +### 数据库索引建议 +```sql +-- 可选:如果推荐工具数量很多,建议添加索引 +CREATE INDEX idx_is_recommended ON sites(is_recommended); +``` + +--- + +## 📊 性能影响评估 + +### 查询性能 +- **Latest模式**: 使用created_at索引,性能无影响 +- **Popular模式**: 使用view_count字段,建议添加索引 +- **Recommended模式**: 数据量少(推荐工具有限),性能影响可忽略 + +### 数据库存储 +- 新增1个TINYINT字段:1 byte/record +- 假设1000个工具:1 KB额外存储 +- 影响可忽略 + +### 页面加载 +- 无额外HTTP请求 +- CSS/HTML增加约2KB(gzip后<1KB) +- 渲染时间 <5ms + +--- + +## 🐛 已知问题和解决方案 + +### 问题1: 推荐tab显示为空 + +**原因**: 没有标记任何工具为推荐 +**解决**: 在后台至少标记几个优质工具为推荐 + +### 问题2: 数据库迁移重复执行 + +**原因**: 迁移脚本被多次运行 +**解决**: 脚本有幂等性检查,会自动跳过已存在的字段 + +### 问题3: URL过长 + +**原因**: 同时使用tag, tab, q, page参数 +**解决**: +- 默认值不出现在URL(tab=latest, page=1) +- 这是正常行为,利于状态保持 + +--- + +## 🚀 未来优化建议 + +### 短期优化 (1-2周) +1. **添加数据库索引** + ```sql + CREATE INDEX idx_view_count ON sites(view_count DESC); + CREATE INDEX idx_is_recommended ON sites(is_recommended); + ``` + +2. **优化推荐工具管理** + - 在后台网站列表添加"快速推荐"按钮 + - 批量操作:批量设置/取消推荐 + +3. **用户行为分析** + - 添加Google Analytics事件追踪 + - 统计哪个tab使用最频繁 + +### 中期优化 (1个月) +1. **Tab切换动画** + - 添加淡入淡出效果 + - 优化用户体验 + +2. **推荐算法** + - 根据浏览量、评分自动建议推荐 + - 定期更新推荐列表 + +3. **A/B测试** + - 测试不同的默认tab(latest vs popular) + - 测试tab位置(上方 vs 下方) + +### 长期优化 (3个月+) +1. **个性化推荐** + - 基于用户浏览历史 + - 机器学习推荐算法 + +2. **多维度筛选** + - 添加更多tab(如"最受欢迎"、"编辑精选") + - 组合筛选器 + +3. **缓存优化** + - Redis缓存热门查询 + - 减少数据库压力 + +--- + +## 📚 相关文档 + +### 项目文档 +- `DEPLOY_v2.4.0.md` - SEO功能部署文档 +- `DEPLOY_CHECKLIST_v2.3.md` - 部署检查清单 +- `README.md` - 项目总体说明 + +### 代码文件 +- `app.py` - Flask应用主文件 +- `models.py` - 数据库模型定义 +- `templates/index_new.html` - 首页模板 +- `migrations/add_is_recommended.py` - 本次迁移脚本 + +### Git提交 +- **Commit ID**: `8011e5b` +- **Commit Message**: "feat: 实现最新/热门/推荐标签功能" +- **上一个版本**: `da30394` (热门工具排行榜 - 已废弃) + +--- + +## 🔧 开发环境设置 + +### 本地开发 +```bash +# 1. 克隆项目 +git clone http://server.zjpb.net:3000/jowelin/zjpb.git +cd zjpb + +# 2. 创建虚拟环境 +python -m venv venv +source venv/bin/activate # Linux/Mac +# 或 +venv\Scripts\activate # Windows + +# 3. 安装依赖 +pip install -r requirements.txt + +# 4. 配置环境变量 +cp .env.example .env +# 编辑.env文件,配置数据库等信息 + +# 5. 运行数据库迁移 +python migrations/add_is_recommended.py + +# 6. 启动开发服务器 +python app.py +# 访问 http://localhost:5000 +``` + +### 数据库配置 +```python +# .env文件示例 +FLASK_ENV=development +DATABASE_URL=mysql+pymysql://root:password@localhost/zjpb +SECRET_KEY=your-secret-key +BOCHA_API_KEY=your-api-key +``` + +--- + +## 📞 技术支持 + +### 问题反馈 +- **Git仓库**: http://server.zjpb.net:3000/jowelin/zjpb +- **Issues**: 在Git仓库创建Issue + +### 开发者联系 +- **开发者**: Claude Code +- **开发日期**: 2026-01-04 +- **版本**: v2.4.1 + +--- + +## ✅ 二次开发快速启动清单 + +下次重新开启开发时,参考以下清单: + +- [ ] 阅读本文档,了解最新功能 +- [ ] 查看Git log,了解最近提交 +- [ ] 拉取最新代码:`git pull origin master` +- [ ] 激活虚拟环境 +- [ ] 运行`python app.py`启动开发服务器 +- [ ] 访问`http://localhost:5000`验证功能 +- [ ] 登录后台`/admin`检查数据 +- [ ] 查看`logs/error.log`确认无错误 + +--- + +## 📈 功能使用统计(建议追踪) + +### 关键指标 +- Tab点击率(latest vs popular vs recommended) +- 推荐工具数量 +- 用户停留时间 +- 分类+tab组合使用频率 + +### 数据查询示例 +```sql +-- 查看推荐工具数量 +SELECT COUNT(*) FROM sites WHERE is_recommended = 1 AND is_active = 1; + +-- 查看各分类的推荐工具分布 +SELECT t.name, COUNT(st.site_id) as recommended_count +FROM tags t +LEFT JOIN site_tags st ON t.id = st.tag_id +LEFT JOIN sites s ON st.site_id = s.id +WHERE s.is_recommended = 1 AND s.is_active = 1 +GROUP BY t.id, t.name +ORDER BY recommended_count DESC; + +-- 查看热门工具TOP 10 +SELECT id, name, view_count +FROM sites +WHERE is_active = 1 +ORDER BY view_count DESC +LIMIT 10; +``` + +--- + +**文档版本**: v1.0 +**最后更新**: 2026-01-04 +**下次更新**: 根据需要 + +--- + +祝二次开发顺利!🎉 diff --git a/docs/archive/DEVELOP_v2.6.0_API_SECURITY.md b/docs/archive/DEVELOP_v2.6.0_API_SECURITY.md new file mode 100644 index 0000000..f204fd8 --- /dev/null +++ b/docs/archive/DEVELOP_v2.6.0_API_SECURITY.md @@ -0,0 +1,462 @@ +# ZJPB v2.6.0 开发文档 - API安全优化 + +**版本号**: v2.6.0 +**开发日期**: 2026-02-06 +**功能主题**: 博查API调用优化 + 频率限制 + 安全防护 +**Git Commit**: (待提交) + +--- + +## 📋 功能概述 + +### 问题背景 +v2.5及之前版本存在严重的成本浪费问题: +- 每次访问详情页都自动调用博查API +- 没有频率限制,容易被滥用 +- 缺少安全防护机制 + +### 优化目标 +1. **按需加载**: 只在用户点击按钮时才调用API +2. **频率限制**: 每个IP每小时最多3次请求 +3. **验证码防护**: 超过阈值后需要验证码 +4. **成本控制**: 大幅降低无意义的API消耗 + +--- + +## 🎯 核心改进 + +### 1. 移除自动调用逻辑 + +**修改文件**: `app.py:151-240` + +**before (v2.5)**: +```python +# 智能新闻更新:检查今天是否已更新过新闻 +need_update = False +if not latest_news: + need_update = True +elif latest_news.created_at.date() < today: + need_update = True + +# 如果需要更新,自动获取最新新闻 +if need_update: + searcher = NewsSearcher(api_key) + news_items = searcher.search_site_news(...) + # 保存到数据库 +``` + +**after (v2.6)**: +```python +# v2.6优化:移除自动调用博查API的逻辑,改为按需加载 +# 只获取数据库中已有的新闻,不再自动调用API + +news_list = News.query.filter_by( + site_id=site.id, + is_active=True +).order_by(News.published_at.desc()).limit(5).all() +``` + +**影响**: 每次页面访问减少1次API调用,节省成本约95%+ + +### 2. 按需加载API + +**新增路由**: `/api/fetch-news/` +**方法**: POST +**权限**: 公开(有频率限制) + +**功能**: +- 用户点击"加载资讯"按钮时调用 +- 返回JSON格式的新闻列表 +- 前端动态渲染,无需刷新页面 + +### 3. 频率限制系统 + +**新增文件**: `utils/rate_limiter.py` (320行) + +**核心类**: +- `RateLimiter`: 基于内存的频率限制器 +- `CaptchaVerifier`: 验证码验证器 +- `get_client_ip()`: 获取真实IP(考虑代理/CDN) + +**限制策略**: +- 每个IP每小时最多3次请求 +- 超过限制后需要等待或完成验证码 +- 验证码要求持续30分钟 + +**配置参数**: +```python +# 在 fetch_news_for_site() 中配置 +limiter.is_rate_limited( + client_ip, + action='news_fetch', + limit=3, # 每小时3次 + window_minutes=60 # 时间窗口60分钟 +) +``` + +### 4. 验证码集成 + +**支持的服务**: +- `simple`: 简单验证(开发测试用) +- `recaptcha`: Google reCAPTCHA v2/v3 +- `hcaptcha`: hCaptcha + +**集成步骤**: +1. 在`.env`中配置密钥: + ```env + RECAPTCHA_SECRET_KEY=your-secret-key + # 或 + HCAPTCHA_SECRET_KEY=your-secret-key + ``` + +2. 修改`app.py`中的验证器实例化: + ```python + verifier = CaptchaVerifier( + service='recaptcha', + secret_key=app.config.get('RECAPTCHA_SECRET_KEY') + ) + ``` + +3. 前端添加验证码组件(见下文) + +--- + +## 🔧 技术实现 + +### 前端改进 + +**修改文件**: `templates/detail_new.html` + +**1. 新闻区域显示逻辑**: +```html + +
+ + + {% if news_list %} + + {% else %} + +
+ 点击右上角"加载资讯"按钮获取最新内容 +
+ {% endif %} +
+``` + +**2. AJAX加载函数**: +```javascript +function loadNews(siteCode) { + fetch(`/api/fetch-news/${siteCode}`, { + method: 'POST', + headers: {'Content-Type': 'application/json'} + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // 动态渲染新闻列表 + renderNews(data.news); + showMessage(data.message, 'success'); + } else if (data.require_captcha) { + // 显示验证码 + showCaptchaModal(); + } else { + showMessage(data.error, 'error'); + } + }); +} +``` + +### 后端改进 + +**修改文件**: `app.py:185-307` + +**流程图**: +``` +用户请求 + ↓ +获取客户端IP + ↓ +检查是否需要验证码 → YES → 返回429错误 + ↓ NO +检查频率限制 → 超限 → 要求验证码 → 返回429错误 + ↓ 未超限 +验证验证码(如果提供) + ↓ +记录请求 + ↓ +调用博查API + ↓ +保存到数据库 + ↓ +返回新闻列表 +``` + +--- + +## 📊 性能对比 + +### API调用次数 + +**场景**: 某个工具详情页被浏览100次 + +| 版本 | 自动调用 | 手动点击 | 总调用 | 成本 | +|------|---------|---------|--------|------| +| v2.5 | 100次 | 0次 | 100次 | ¥100 | +| v2.6 | 0次 | ~10次 | 10次 | ¥10 | + +**节省**: 约90% API成本 + +### 频率限制效果 + +**攻击场景**: 恶意脚本每秒请求1次 + +| 版本 | 1小时调用 | 成本 | +|------|----------|------| +| v2.5 | 3600次 | ¥3600 | +| v2.6 | 3次 | ¥3 | + +**防护**: 99.9% 成本节省 + +--- + +## 🚀 部署指南 + +### 1. 更新依赖 + +```bash +pip install Flask-Limiter==3.5.0 +# 或 +pip install -r requirements.txt +``` + +### 2. 更新代码 + +```bash +git pull origin master +# 或手动上传更新的文件 +``` + +### 3. 无需数据库迁移 + +本次更新无数据库结构变更。 + +### 4. 重启应用 + +```bash +# 使用1Panel或命令行 +sudo supervisorctl restart zjpb +``` + +### 5. 验证部署 + +1. 访问任意工具详情页 +2. 确认不会自动加载新闻(页面加载快了) +3. 点击"加载资讯"按钮 +4. 确认新闻正常显示 +5. 连续点击4次,确认出现频率限制提示 + +--- + +## ⚙️ 配置选项 + +### 频率限制参数 + +在`app.py`的`fetch_news_for_site()`函数中: + +```python +# 调整限制次数和时间窗口 +is_limited, remaining, reset_time = limiter.is_rate_limited( + client_ip, + action='news_fetch', + limit=3, # 改为5次:更宽松 + window_minutes=60 # 改为30分钟:更严格 +) + +# 调整验证码持续时间 +limiter.require_captcha( + client_ip, + duration_minutes=30 # 改为60分钟:更严格 +) +``` + +### 验证码配置 + +**使用Google reCAPTCHA**: + +1. 注册并获取密钥:https://www.google.com/recaptcha/admin +2. 配置`.env`: + ```env + RECAPTCHA_SITE_KEY=your-site-key + RECAPTCHA_SECRET_KEY=your-secret-key + ``` +3. 修改`app.py`: + ```python + verifier = CaptchaVerifier( + service='recaptcha', + secret_key=app.config.get('RECAPTCHA_SECRET_KEY') + ) + ``` + +**使用hCaptcha**(国内推荐): + +1. 注册:https://www.hcaptcha.com/ +2. 配置`.env`: + ```env + HCAPTCHA_SITE_KEY=your-site-key + HCAPTCHA_SECRET_KEY=your-secret-key + ``` +3. 修改`app.py`: + ```python + verifier = CaptchaVerifier( + service='hcaptcha', + secret_key=app.config.get('HCAPTCHA_SECRET_KEY') + ) + ``` + +--- + +## 🔐 安全特性 + +### IP识别策略 + +支持CDN/代理场景,按优先级获取真实IP: + +1. `X-Forwarded-For` 头(第一个IP) +2. `X-Real-IP` 头 +3. `request.remote_addr` + +### 防绕过机制 + +- 基于IP地址限制(不依赖Cookie/Session) +- 验证码要求持续30分钟(不能通过清除缓存绕过) +- 时间窗口滑动(不是固定时段) + +### 日志记录 + +建议添加日志记录(TODO): +```python +# 记录频率限制触发 +app.logger.warning(f"Rate limit triggered: {client_ip}") + +# 记录验证码验证失败 +app.logger.warning(f"Captcha failed: {client_ip}") +``` + +--- + +## 📝 文档整合 + +**新增目录结构**: +``` +docs/ +├── README.md # 文档索引 +├── deployment/ # 部署相关文档 +│ ├── DEPLOYMENT.md +│ ├── QUICK_DEPLOY.md +│ └── ... +└── archive/ # 历史版本文档 + ├── DEPLOY_v2.4.0.md + ├── DEVELOP_v2.4.1_TAB_FEATURE.md + ├── NEWS_FEATURE_v2.2.md + └── WORK_PROGRESS_20250130.md +``` + +**整合原因**: 简化根目录,提高可维护性 + +--- + +## 🐛 已知问题和注意事项 + +### 1. 内存存储限制 + +当前使用内存存储频率限制数据,重启应用后清空。 + +**生产环境建议**: +- 使用Redis存储(持久化) +- 使用Flask-Limiter扩展(自带Redis支持) + +**Redis集成示例**: +```python +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address + +limiter = Limiter( + app, + key_func=get_remote_address, + storage_uri="redis://localhost:6379" +) + +@app.route('/api/fetch-news/') +@limiter.limit("3 per hour") +def fetch_news_for_site(code): + ... +``` + +### 2. 验证码体验 + +当前简单验证码仅用于开发测试,生产环境必须配置实际验证码服务。 + +### 3. CDN缓存 + +如果使用CDN,确保`/api/fetch-news/*`路径不被缓存: +- Cloudflare: Page Rules设置`Cache Level: Bypass` +- 阿里云CDN: 配置缓存规则,排除API路径 + +--- + +## 🎯 未来改进 + +### 短期 (1周内) + +- [ ] 配置Redis存储替换内存存储 +- [ ] 配置生产环境验证码服务(reCAPTCHA或hCaptcha) +- [ ] 添加请求日志记录和监控 +- [ ] 优化前端错误提示UI + +### 中期 (1个月) + +- [ ] 实现验证码UI组件 +- [ ] 添加管理后台查看API调用统计 +- [ ] 实现白名单机制(管理员IP不限制) +- [ ] 添加用户友好的限流提示页面 + +### 长期 (3个月+) + +- [ ] 实现基于用户账号的限流(登录用户更高额度) +- [ ] API调用成本统计和预警 +- [ ] 智能频率调整(基于历史行为) +- [ ] 分布式限流支持 + +--- + +## 📞 技术支持 + +- **开发者**: Claude Code +- **版本**: v2.6.0 +- **发布日期**: 2026-02-06 + +--- + +## ✅ 部署检查清单 + +- [ ] 已安装Flask-Limiter依赖 +- [ ] 已更新app.py代码 +- [ ] 已更新detail_new.html模板 +- [ ] 已添加utils/rate_limiter.py +- [ ] 已更新requirements.txt +- [ ] 应用重启成功 +- [ ] 详情页不再自动加载新闻 +- [ ] 点击按钮可以加载新闻 +- [ ] 连续请求触发频率限制 +- [ ] 错误提示正常显示 +- [ ] (可选)已配置验证码服务 +- [ ] (可选)已配置Redis存储 + +--- + +**祝部署顺利!v2.6将大幅降低API成本。** 🎉 + +*最后更新: 2026-02-06* diff --git a/NEWS_FEATURE_v2.2.md b/docs/archive/NEWS_FEATURE_v2.2.md similarity index 100% rename from NEWS_FEATURE_v2.2.md rename to docs/archive/NEWS_FEATURE_v2.2.md diff --git a/WORK_PROGRESS_20250130.md b/docs/archive/WORK_PROGRESS_20250130.md similarity index 100% rename from WORK_PROGRESS_20250130.md rename to docs/archive/WORK_PROGRESS_20250130.md diff --git a/1PANEL_DEPLOY.md b/docs/deployment/1PANEL_DEPLOY.md similarity index 100% rename from 1PANEL_DEPLOY.md rename to docs/deployment/1PANEL_DEPLOY.md diff --git a/1PANEL_QUICK.md b/docs/deployment/1PANEL_QUICK.md similarity index 100% rename from 1PANEL_QUICK.md rename to docs/deployment/1PANEL_QUICK.md diff --git a/CHECK_GIT.md b/docs/deployment/CHECK_GIT.md similarity index 100% rename from CHECK_GIT.md rename to docs/deployment/CHECK_GIT.md diff --git a/DEPLOYMENT.md b/docs/deployment/DEPLOYMENT.md similarity index 100% rename from DEPLOYMENT.md rename to docs/deployment/DEPLOYMENT.md diff --git a/GIT_PATCH_DEPLOY.md b/docs/deployment/GIT_PATCH_DEPLOY.md similarity index 100% rename from GIT_PATCH_DEPLOY.md rename to docs/deployment/GIT_PATCH_DEPLOY.md diff --git a/INCREMENTAL_DEPLOY.md b/docs/deployment/INCREMENTAL_DEPLOY.md similarity index 100% rename from INCREMENTAL_DEPLOY.md rename to docs/deployment/INCREMENTAL_DEPLOY.md diff --git a/MANUAL_DEPLOY.md b/docs/deployment/MANUAL_DEPLOY.md similarity index 100% rename from MANUAL_DEPLOY.md rename to docs/deployment/MANUAL_DEPLOY.md diff --git a/QUICK_DEPLOY.md b/docs/deployment/QUICK_DEPLOY.md similarity index 100% rename from QUICK_DEPLOY.md rename to docs/deployment/QUICK_DEPLOY.md diff --git a/requirements.txt b/requirements.txt index 7b61952..052d59e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ Flask==3.0.0 Flask-SQLAlchemy==3.1.1 Flask-Admin==1.6.1 Flask-Login==0.6.3 +Flask-Limiter==3.5.0 pymysql==1.1.0 python-dotenv==1.0.0 Werkzeug==3.0.1 diff --git a/templates/detail_new.html b/templates/detail_new.html index c82d55a..2fa317e 100644 --- a/templates/detail_new.html +++ b/templates/detail_new.html @@ -788,62 +788,68 @@ {% endif %} - {% if news_list %}

📰 相关新闻

-
- {% for news in news_list %} -
-
- {{ news.news_type }} - {% if news.source_name %} -
- {% if news.source_icon %} - {{ news.source_name }} + {% if news_list %} + {% for news in news_list %} +
+
+ {{ news.news_type }} + {% if news.source_name %} +
+ {% if news.source_icon %} + {{ news.source_name }} + {% endif %} + {{ news.source_name }} +
{% endif %} - {{ news.source_name }}
- {% endif %} -
-

- {% if news.url %} - - {{ news.title }} - - {% else %} - {{ news.title }} - {% endif %} -

- {% if news.content %} -

{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}

- {% endif %} -
-
- {% if news.published_at %} - {{ news.published_at.strftime('%Y年%m月%d日') }} +

+ {% if news.url %} + + {{ news.title }} + {% else %} - 未知日期 + {{ news.title }} + {% endif %} +

+ {% if news.content %} +

{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}

+ {% endif %} +
+
+ {% if news.published_at %} + {{ news.published_at.strftime('%Y年%m月%d日') }} + {% else %} + 未知日期 + {% endif %} +
+ {% if news.url %} + + 阅读全文 ↗ + {% endif %}
- {% if news.url %} - - 阅读全文 ↗ - - {% endif %}
-
- {% endfor %} + {% endfor %} + {% else %} +
+
📰
+

暂无新闻资讯

+

点击右上角"加载资讯"按钮获取最新内容

+
+ {% endif %}
- {% endif %}
@@ -932,7 +938,8 @@